@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.1

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 (161) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/app.plugin.js +42 -39
  5. package/dist/AtomHost.js +18 -11
  6. package/dist/EditorAddon.js +4 -2
  7. package/dist/EditorAddonNormalization.js +17 -9
  8. package/dist/EditorMentionThemeNormalization.js +6 -3
  9. package/dist/EditorMentionThemeValidation.js +28 -11
  10. package/dist/EditorStyleSheetNormalization.js +84 -37
  11. package/dist/EditorTheme.d.ts +4 -0
  12. package/dist/EditorTheme.js +5 -3
  13. package/dist/EditorToolbarComponent.d.ts +1 -1
  14. package/dist/EditorToolbarComponent.js +1 -1
  15. package/dist/EditorToolbarRegistry.js +5 -5
  16. package/dist/EditorToolbarTypes.d.ts +12 -12
  17. package/dist/EditorToolbarVisuals.js +1 -1
  18. package/dist/ExternalTextComposition.js +89 -43
  19. package/dist/ImageLoadingPolicy.d.ts +1 -1
  20. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  21. package/dist/NativeEditorBoundaryError.js +9 -5
  22. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  23. package/dist/NativeEditorCollaborationTransport.js +28 -15
  24. package/dist/NativeEditorCreateConfig.js +11 -6
  25. package/dist/NativeEditorCreateJson.js +51 -25
  26. package/dist/NativeEditorCreateValidation.js +11 -6
  27. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  28. package/dist/NativeEditorDocumentBridge.js +32 -18
  29. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  30. package/dist/NativeEditorDocumentHandle.js +1 -1
  31. package/dist/NativeEditorLocalAwareness.js +25 -13
  32. package/dist/NativeEditorNativeModule.d.ts +8 -1
  33. package/dist/NativeEditorNativeModule.js +7 -5
  34. package/dist/NativeEditorRenderNormalization.js +66 -25
  35. package/dist/NativeEditorResultNormalization.js +53 -29
  36. package/dist/NativeEditorTypes.d.ts +10 -10
  37. package/dist/NativeEditorV2Decimal.js +6 -3
  38. package/dist/NativeProseViewer.js +11 -3
  39. package/dist/ResourceLimits.d.ts +2 -2
  40. package/dist/ResourceLimits.js +7 -2
  41. package/dist/RichTextEditor.js +1 -1
  42. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  43. package/dist/RichTextEditorNativeView.d.ts +1 -3
  44. package/dist/RichTextEditorSerialization.js +24 -14
  45. package/dist/RichTextEditorTypes.d.ts +1 -1
  46. package/dist/YjsCollaboration.d.ts +7 -7
  47. package/dist/YjsCollaboration.js +42 -26
  48. package/dist/addons.d.ts +4 -4
  49. package/dist/addons.js +3 -3
  50. package/dist/atomInstances.js +6 -3
  51. package/dist/atomUpdates.js +7 -4
  52. package/dist/atoms.js +20 -12
  53. package/dist/attributeValidation.js +55 -24
  54. package/dist/contentExpression.js +80 -42
  55. package/dist/listMarkers.d.ts +1 -1
  56. package/dist/listMarkers.js +1 -1
  57. package/dist/schemaDefinition.d.ts +1 -1
  58. package/dist/schemaDefinition.js +23 -14
  59. package/dist/schemaDocument.js +1 -1
  60. package/dist/schemaDocumentConstruction.js +25 -14
  61. package/dist/schemaNormalization.js +51 -29
  62. package/dist/schemaPresets.d.ts +1 -1
  63. package/dist/schemaPresets.js +9 -4
  64. package/dist/schemaResolution.js +46 -25
  65. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  66. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  67. package/dist/useEditorToolbarInteractions.js +45 -23
  68. package/dist/useEditorToolbarItems.d.ts +1 -1
  69. package/dist/useEditorToolbarItems.js +4 -3
  70. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  71. package/dist/useEditorToolbarPresentation.js +4 -4
  72. package/dist/useEditorToolbarState.d.ts +1 -1
  73. package/dist/useEditorToolbarState.js +1 -1
  74. package/dist/useFocusPreservingFrames.js +11 -8
  75. package/dist/useNativeEditor.d.ts +1 -1
  76. package/dist/useNativeEditor.js +21 -11
  77. package/dist/useRichTextEditorCommands.d.ts +2 -2
  78. package/dist/useRichTextEditorCommands.js +59 -29
  79. package/dist/useRichTextEditorEvents.d.ts +3 -3
  80. package/dist/useRichTextEditorEvents.js +75 -32
  81. package/dist/useRichTextEditorMentions.d.ts +4 -4
  82. package/dist/useRichTextEditorMentions.js +48 -29
  83. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  84. package/dist/useRichTextEditorPresentation.js +25 -23
  85. package/dist/useRichTextEditorState.d.ts +3 -3
  86. package/dist/useRichTextEditorState.js +20 -15
  87. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  88. package/dist/useRichTextEditorUpdates.js +99 -41
  89. package/dist/useViewerAtoms.js +63 -36
  90. package/ios/EditorCore.xcframework/Info.plist +5 -5
  91. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  92. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  93. package/package.json +11 -3
  94. package/react-native.config.js +1 -1
  95. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  96. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  97. package/rust/android/x86/libeditor_core.so +0 -0
  98. package/rust/android/x86_64/libeditor_core.so +0 -0
  99. package/src/AtomHost.tsx +58 -28
  100. package/src/EditorAddon.ts +18 -8
  101. package/src/EditorAddonNormalization.ts +40 -11
  102. package/src/EditorMentionThemeNormalization.ts +15 -4
  103. package/src/EditorMentionThemeValidation.ts +51 -14
  104. package/src/EditorStyleSheet.ts +1 -0
  105. package/src/EditorStyleSheetNormalization.ts +195 -64
  106. package/src/EditorTheme.ts +13 -5
  107. package/src/EditorToolbarComponent.tsx +2 -1
  108. package/src/EditorToolbarRegistry.tsx +23 -13
  109. package/src/EditorToolbarTypes.tsx +12 -12
  110. package/src/EditorToolbarVisuals.tsx +6 -3
  111. package/src/EditorUpdateRevision.ts +1 -0
  112. package/src/ExternalTextComposition.ts +259 -49
  113. package/src/ImageLoadingPolicy.ts +3 -1
  114. package/src/NativeEditorBoundaryError.ts +38 -11
  115. package/src/NativeEditorCollaborationTransport.ts +108 -27
  116. package/src/NativeEditorCreateConfig.ts +92 -19
  117. package/src/NativeEditorCreateJson.ts +163 -26
  118. package/src/NativeEditorCreateValidation.ts +33 -12
  119. package/src/NativeEditorDocumentBridge.ts +111 -19
  120. package/src/NativeEditorDocumentHandle.ts +9 -4
  121. package/src/NativeEditorLocalAwareness.ts +70 -15
  122. package/src/NativeEditorNativeModule.ts +13 -9
  123. package/src/NativeEditorRenderNormalization.ts +128 -29
  124. package/src/NativeEditorResultNormalization.ts +143 -32
  125. package/src/NativeEditorTypes.ts +12 -11
  126. package/src/NativeEditorV2Decimal.ts +12 -3
  127. package/src/NativeProseViewer.tsx +46 -16
  128. package/src/ResourceLimits.ts +15 -8
  129. package/src/RichTextEditor.tsx +3 -1
  130. package/src/RichTextEditorNativeTypes.tsx +1 -1
  131. package/src/RichTextEditorNativeView.tsx +2 -8
  132. package/src/RichTextEditorSerialization.tsx +70 -24
  133. package/src/RichTextEditorTypes.tsx +1 -1
  134. package/src/YjsCollaboration.ts +143 -47
  135. package/src/addons.ts +12 -8
  136. package/src/atomInstances.ts +25 -5
  137. package/src/atomUpdates.ts +23 -6
  138. package/src/atoms.ts +85 -18
  139. package/src/attributeValidation.ts +97 -33
  140. package/src/contentExpression.ts +218 -63
  141. package/src/listMarkers.ts +1 -1
  142. package/src/schemaDefinition.ts +82 -20
  143. package/src/schemaDocument.ts +15 -1
  144. package/src/schemaDocumentConstruction.ts +80 -16
  145. package/src/schemaNormalization.ts +155 -41
  146. package/src/schemaPresets.ts +37 -31
  147. package/src/schemaResolution.ts +126 -27
  148. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  149. package/src/useEditorToolbarInteractions.tsx +85 -36
  150. package/src/useEditorToolbarItems.tsx +50 -22
  151. package/src/useEditorToolbarPresentation.tsx +81 -58
  152. package/src/useEditorToolbarState.tsx +8 -6
  153. package/src/useFocusPreservingFrames.ts +43 -15
  154. package/src/useNativeEditor.ts +111 -51
  155. package/src/useRichTextEditorCommands.tsx +127 -54
  156. package/src/useRichTextEditorEvents.tsx +144 -42
  157. package/src/useRichTextEditorMentions.tsx +118 -37
  158. package/src/useRichTextEditorPresentation.tsx +128 -114
  159. package/src/useRichTextEditorState.tsx +76 -40
  160. package/src/useRichTextEditorUpdates.tsx +207 -56
  161. package/src/useViewerAtoms.tsx +162 -66
@@ -15,8 +15,9 @@ function isRecord(value) {
15
15
  }
16
16
  function parsePositions(json, definitions, snapshot) {
17
17
  const values = JSON.parse(json);
18
- if (!Array.isArray(values))
18
+ if (!Array.isArray(values)) {
19
19
  throw new Error('Atom positions must be an array.');
20
+ }
20
21
  const seen = new Set();
21
22
  const identities = new Set();
22
23
  return values.map((value) => {
@@ -27,24 +28,27 @@ function parsePositions(json, definitions, snapshot) {
27
28
  Number(value.docPos) < 0 ||
28
29
  Number(value.docPos) > 0xffffffff ||
29
30
  typeof value.attrsJson !== 'string' ||
30
- !['x', 'y', 'width', 'height'].every((key) => typeof value[key] === 'number' && Number.isFinite(value[key])) ||
31
+ !['x', 'y', 'width', 'height'].every(key => typeof value[key] === 'number' && Number.isFinite(value[key])) ||
31
32
  Number(value.width) <= 0 ||
32
33
  Number(value.height) < 0 ||
33
34
  seen.has(Number(value.docPos))) {
34
35
  throw new Error('Invalid prepared atom position.');
35
36
  }
36
37
  const attrs = JSON.parse(value.attrsJson);
37
- if (!isRecord(attrs))
38
+ if (!isRecord(attrs)) {
38
39
  throw new Error('Atom attributes must be a JSON object.');
40
+ }
39
41
  const idAttribute = definitions.get(value.nodeType)?.idAttribute;
40
42
  const atomId = idAttribute == null ? undefined : attrs[idAttribute];
41
- if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0))
43
+ if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0)) {
42
44
  throw new Error('Atom identity must be a non-empty string.');
45
+ }
43
46
  const key = typeof atomId === 'string'
44
47
  ? JSON.stringify([value.nodeType, atomId])
45
- : `${snapshot}:${value.docPos}:${value.nodeType}`;
46
- if (identities.has(key))
48
+ : `${snapshot}:${String(value.docPos)}:${value.nodeType}`;
49
+ if (identities.has(key)) {
47
50
  throw new Error('Duplicate atom identity.');
51
+ }
48
52
  identities.add(key);
49
53
  seen.add(Number(value.docPos));
50
54
  return { ...value, attrs, key, atomId };
@@ -52,18 +56,20 @@ function parsePositions(json, definitions, snapshot) {
52
56
  }
53
57
  function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = true, viewport, onUpdateAtomAttrs, onError, }) {
54
58
  const [width, setWidth] = (0, react_1.useState)(null);
55
- const snapshot = (0, react_1.useMemo)(() => String(++nextGeneration), [identity]);
59
+ const snapshot = (0, react_1.useMemo)(() => ({ identity, value: String(++nextGeneration) }), [identity]).value;
56
60
  const serializedAtoms = (0, atoms_1.serializeEditorAtoms)(atoms);
57
- const identifiers = JSON.stringify((atoms ?? []).map((atom) => [atom.name, atom.idAttribute]));
58
- const generation = (0, react_1.useMemo)(() => String(++nextGeneration), [identity, identifiers, serializedAtoms, width]);
59
- const definitions = (0, react_1.useMemo)(() => new Map((atoms ?? []).map((atom) => [atom.name, atom])), [atoms]);
61
+ const identifiers = JSON.stringify((atoms ?? []).map(atom => [atom.name, atom.idAttribute]));
62
+ const generation = (0, react_1.useMemo)(() => ({
63
+ identity, identifiers, serializedAtoms, width, value: String(++nextGeneration),
64
+ }), [identity, identifiers, serializedAtoms, width]).value;
65
+ const definitions = (0, react_1.useMemo)(() => new Map((atoms ?? []).map(atom => [atom.name, atom])), [atoms]);
60
66
  const enabled = definitions.size > 0;
61
67
  const [measurementState, setMeasurementState] = (0, react_1.useState)({
62
68
  generation,
63
69
  revision: 0,
64
70
  values: {},
65
71
  });
66
- const measurements = measurementState.generation === generation ? measurementState.values : {};
72
+ const measurements = (0, react_1.useMemo)(() => measurementState.generation === generation ? measurementState.values : {}, [measurementState, generation]);
67
73
  const revision = String(measurementState.generation === generation ? measurementState.revision : 0);
68
74
  const [layout, setLayout] = (0, react_1.useState)(null);
69
75
  const pendingLayout = layout?.generation !== generation;
@@ -72,8 +78,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
72
78
  const mountedMeasurements = (0, react_1.useRef)(new Map());
73
79
  (0, react_1.useLayoutEffect)(() => {
74
80
  for (const [key, measured] of mountedMeasurements.current) {
75
- if (!definitions.has(measured.nodeType))
81
+ if (!definitions.has(measured.nodeType)) {
76
82
  mountedMeasurements.current.delete(key);
83
+ }
77
84
  }
78
85
  }, [definitions]);
79
86
  const mounted = (0, react_1.useRef)(false);
@@ -119,7 +126,12 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
119
126
  measurements,
120
127
  },
121
128
  })
122
- : themeJson, [enabled, themeJson, serializedAtoms, generation, revision, measurements]);
129
+ : themeJson, [enabled,
130
+ themeJson,
131
+ serializedAtoms,
132
+ generation,
133
+ revision,
134
+ measurements]);
123
135
  const onAtomLayout = (0, react_1.useCallback)((event) => {
124
136
  const value = event.nativeEvent;
125
137
  const latest = current.current;
@@ -128,8 +140,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
128
140
  value.revision !== latest.revision ||
129
141
  !Number.isFinite(value.layoutWidth) ||
130
142
  value.layoutWidth <= 0 ||
131
- (latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1))
143
+ (latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1)) {
132
144
  return;
145
+ }
133
146
  try {
134
147
  const next = parsePositions(value.atomsJson, latest.definitions, latest.snapshot);
135
148
  const retainedMeasurements = {};
@@ -143,18 +156,19 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
143
156
  }
144
157
  }
145
158
  for (const key of mountedMeasurements.current.keys()) {
146
- if (!retainedKeys.has(key))
159
+ if (!retainedKeys.has(key)) {
147
160
  mountedMeasurements.current.delete(key);
161
+ }
148
162
  }
149
163
  // Unchanged mounted sizes do not emit another Yoga onLayout event.
150
- setMeasurementState((previous) => previous.generation === value.generation
164
+ setMeasurementState(previous => previous.generation === value.generation
151
165
  ? previous
152
166
  : {
153
167
  generation: value.generation,
154
168
  revision: Object.keys(retainedMeasurements).length > 0 ? 1 : 0,
155
169
  values: retainedMeasurements,
156
170
  });
157
- setLayout((previous) => previous?.generation === value.generation && previous.json === value.atomsJson
171
+ setLayout(previous => previous?.generation === value.generation && previous.json === value.atomsJson
158
172
  ? previous
159
173
  : { generation: value.generation, json: value.atomsJson, positions: next });
160
174
  }
@@ -171,8 +185,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
171
185
  }, []);
172
186
  const onContainerLayout = (0, react_1.useCallback)((event) => {
173
187
  const next = event.nativeEvent.layout.width;
174
- if (Number.isFinite(next) && next > 0)
175
- setWidth((previous) => (previous === next ? previous : next));
188
+ if (Number.isFinite(next) && next > 0) {
189
+ setWidth(previous => (previous === next ? previous : next));
190
+ }
176
191
  }, []);
177
192
  const updateQueues = (0, react_1.useRef)(new Map());
178
193
  const acknowledged = (0, react_1.useRef)(new Map());
@@ -182,14 +197,16 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
182
197
  const updateAttrs = (0, react_1.useCallback)((owner, atom, update) => {
183
198
  const execute = async () => {
184
199
  const latest = current.current;
185
- if (!mounted.current)
200
+ if (!mounted.current) {
186
201
  throw new atomInstances_1.AtomUpdateAttrsError('not-ready', 'The viewer is unmounted.');
202
+ }
187
203
  if (owner !== latest.generation ||
188
- !latest.positions.some((position) => position.key === atom.key && position.attrsJson === atom.attrsJson)) {
204
+ !latest.positions.some(position => position.key === atom.key && position.attrsJson === atom.attrsJson)) {
189
205
  throw new atomInstances_1.AtomUpdateAttrsError('stale-revision', 'The viewer content has changed.');
190
206
  }
191
- if (latest.readOnly || !latest.onUpdateAtomAttrs)
207
+ if (latest.readOnly || !latest.onUpdateAtomAttrs) {
192
208
  throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', 'Viewer updates require readOnly={false} and onUpdateAtomAttrs.');
209
+ }
193
210
  const accepted = acknowledged.current.get(atom.key);
194
211
  const attrs = accepted?.generation === owner ? accepted.attrs : atom.attrs;
195
212
  const partial = (0, atomUpdates_1.resolveAtomAttrsUpdate)(attrs, update);
@@ -198,8 +215,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
198
215
  (0, attributeValidation_1.validateAttributes)({ ...attrs, ...partial }, definition.attrs ?? {});
199
216
  if (definition.idAttribute &&
200
217
  Object.prototype.hasOwnProperty.call(partial, definition.idAttribute) &&
201
- partial[definition.idAttribute] !== attrs[definition.idAttribute])
218
+ partial[definition.idAttribute] !== attrs[definition.idAttribute]) {
202
219
  throw new Error('An atom identity cannot be changed.');
220
+ }
203
221
  }
204
222
  catch (error) {
205
223
  throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', error instanceof Error ? error.message : String(error));
@@ -211,18 +229,21 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
211
229
  attrs,
212
230
  partial,
213
231
  });
214
- if (mounted.current && current.current.generation === owner)
232
+ if (mounted.current && current.current.generation === owner) {
215
233
  acknowledged.current.set(atom.key, {
216
234
  generation: owner,
217
235
  attrs: { ...attrs, ...partial },
218
236
  });
237
+ }
219
238
  };
220
239
  const previous = updateQueues.current.get(atom.key);
221
- const result = previous ? previous.catch(() => { }).then(execute) : execute();
240
+ const result = previous ? previous.catch(() => {
241
+ }).then(execute) : execute();
222
242
  updateQueues.current.set(atom.key, result);
223
243
  const clean = () => {
224
- if (updateQueues.current.get(atom.key) === result)
244
+ if (updateQueues.current.get(atom.key) === result) {
225
245
  updateQueues.current.delete(atom.key);
246
+ }
226
247
  };
227
248
  void result.then(clean, clean);
228
249
  return result;
@@ -238,22 +259,25 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
238
259
  !Number.isFinite(measured.height) ||
239
260
  measured.height < 0 ||
240
261
  Math.abs(measured.width - atom.width) > 1 ||
241
- !latest.renderedPositions.some((position) => position.docPos === atom.docPos &&
262
+ !latest.renderedPositions.some(position => position.docPos === atom.docPos &&
242
263
  position.nodeType === atom.nodeType &&
243
264
  position.width === atom.width &&
244
- position.attrsJson === atom.attrsJson))
265
+ position.attrsJson === atom.attrsJson)) {
245
266
  return;
267
+ }
246
268
  mountedMeasurements.current.set(atom.key, {
247
269
  nodeType: atom.nodeType,
248
270
  size: { width: atom.width, height: measured.height },
249
271
  });
250
- if (latest.pendingLayout)
272
+ if (latest.pendingLayout) {
251
273
  return;
252
- setMeasurementState((previous) => {
274
+ }
275
+ setMeasurementState(previous => {
253
276
  const values = previous.generation === owner ? previous.values : {};
254
277
  const existing = values[String(atom.docPos)];
255
- if (existing?.width === atom.width && existing.height === measured.height)
278
+ if (existing?.width === atom.width && existing.height === measured.height) {
256
279
  return previous;
280
+ }
257
281
  return {
258
282
  generation: owner,
259
283
  revision: previous.generation === owner ? previous.revision + 1 : 1,
@@ -264,25 +288,28 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
264
288
  };
265
289
  });
266
290
  }, []);
267
- const children = renderedPositions.map((atom) => {
291
+ const children = renderedPositions.map(atom => {
268
292
  const Component = definitions.get(atom.nodeType)?.component;
269
- if (!Component)
293
+ if (!Component) {
270
294
  return null;
295
+ }
271
296
  return ((0, jsx_runtime_1.jsx)(react_native_1.View, { collapsable: false, pointerEvents: !interactive || pendingLayout ? 'none' : 'box-none', accessibilityElementsHidden: pendingLayout, importantForAccessibility: pendingLayout ? 'no-hide-descendants' : 'auto', style: {
272
297
  position: 'absolute',
273
298
  left: atom.x,
274
299
  top: atom.y,
275
300
  width: atom.width,
276
301
  opacity: pendingLayout ? 0 : 1,
277
- }, onLayout: (event) => measure(generation, atom, Component, event), children: (0, jsx_runtime_1.jsx)(AtomHost_1.AtomHost, { component: Component, width: atom.width, estimatedHeight: atom.height, visible: (0, AtomHost_1.atomIsVisible)(atom.y, atom.height, viewport), onMeasure: (event) => measure(generation, atom, Component, event), atomProps: {
302
+ }, onLayout: event => measure(generation, atom, Component, event), children: (0, jsx_runtime_1.jsx)(AtomHost_1.AtomHost, { component: Component, width: atom.width, estimatedHeight: atom.height, visible: (0, AtomHost_1.atomIsVisible)(atom.y, atom.height, viewport), onMeasure: event => measure(generation, atom, Component, event), atomProps: {
278
303
  attrs: atom.attrs,
279
304
  nodeType: atom.nodeType,
280
305
  selected: false,
281
306
  readOnly,
282
307
  interactive: interactive && !pendingLayout,
283
308
  isViewer: true,
284
- updateAttrs: (partial) => updateAttrs(layout.generation, atom, partial),
309
+ updateAttrs: partial => updateAttrs(layout.generation, atom, partial),
285
310
  } }) }, atom.key));
286
311
  });
287
- return { enabled, themeJson: configuredThemeJson, onAtomLayout, onContainerLayout, children };
312
+ return {
313
+ enabled, themeJson: configuredThemeJson, onAtomLayout, onContainerLayout, children,
314
+ };
288
315
  }
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>libeditor_core.a</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>libeditor_core.a</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-rich-text-editor",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Native rich text editor with Rust core for React Native",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/apollohg/react-native-rich-text-editor",
@@ -104,21 +104,29 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "@babel/runtime": "^7.29.7",
107
+ "@eslint/js": "^10.0.1",
107
108
  "@expo/vector-icons": "^15.0.3",
108
109
  "@react-native/jest-preset": "0.86.2",
110
+ "@stylistic/eslint-plugin": "^5.10.0",
109
111
  "@testing-library/react-native": "^13.3.3",
110
112
  "@types/jest": "29.5.14",
111
113
  "@types/react": "^19.2.14",
112
114
  "babel-jest": "^29.7.0",
113
115
  "babel-preset-expo": "~57.0.8",
116
+ "eslint": "^10.10.0",
117
+ "eslint-plugin-react": "^7.37.5",
118
+ "eslint-plugin-react-hooks": "^7.1.1",
119
+ "eslint-plugin-react-native": "^5.0.0",
120
+ "eslint-plugin-slop": "^0.1.3",
114
121
  "expo": "^57.0.9",
115
122
  "expo-modules-core": "~57.0.8",
116
123
  "jest": "^29.7.0",
117
- "prettier": "^3.8.1",
124
+ "jiti": "^2.7.0",
118
125
  "react": "19.2.3",
119
126
  "react-native": "0.86.2",
120
127
  "react-test-renderer": "19.2.3",
121
- "typescript": "^5.9.3"
128
+ "typescript": "^5.9.3",
129
+ "typescript-eslint": "^8.70.0"
122
130
  },
123
131
  "files": [
124
132
  "README.md",
@@ -7,7 +7,7 @@ module.exports = {
7
7
  // metadata; no nested compatibility podspec exists.
8
8
  ios: {},
9
9
  android: {
10
- componentDescriptors: ['PreparedProseViewerComponentDescriptor'],
10
+ componentDescriptors: [ 'PreparedProseViewerComponentDescriptor' ],
11
11
  cmakeListsPath: '../android/src/main/jni/CMakeLists.txt',
12
12
  packageImportPath: 'import com.apollohg.editor.viewer.PreparedProseViewerPackage;',
13
13
  packageInstance: 'new PreparedProseViewerPackage()',
Binary file
package/src/AtomHost.tsx CHANGED
@@ -11,9 +11,12 @@ export interface AtomViewport {
11
11
  }
12
12
 
13
13
  export function atomIsVisible(y: number, height: number, viewport?: AtomViewport): boolean {
14
- if (!viewport) return true;
14
+ if (!viewport) {
15
+ return true;
16
+ }
17
+
15
18
  if (
16
- ![viewport.y, viewport.height, viewport.overscan ?? 200].every(Number.isFinite) ||
19
+ ![ viewport.y, viewport.height, viewport.overscan ?? 200 ].every(Number.isFinite) ||
17
20
  viewport.height < 0 ||
18
21
  (viewport.overscan ?? 200) < 0
19
22
  ) {
@@ -21,7 +24,9 @@ export function atomIsVisible(y: number, height: number, viewport?: AtomViewport
21
24
  'Atom viewport must have finite coordinates and non-negative height and overscan.'
22
25
  );
23
26
  }
27
+
24
28
  const overscan = viewport.overscan ?? 200;
29
+
25
30
  return y + height >= viewport.y - overscan && y <= viewport.y + viewport.height + overscan;
26
31
  }
27
32
 
@@ -49,13 +54,17 @@ class AtomErrorBoundary extends React.Component<
49
54
  }
50
55
 
51
56
  render() {
52
- if (!this.state.error) return this.props.children;
57
+ if (!this.state.error) {
58
+ return this.props.children;
59
+ }
60
+
53
61
  return (
54
- <View accessibilityRole='alert' style={{ minHeight: 32, padding: 8 }}>
62
+ <View accessibilityRole={'alert'} style={{ minHeight: 32, padding: 8 }}>
55
63
  <Text>Unable to display {this.props.nodeType}</Text>
56
64
  <Pressable
57
- accessibilityRole='button'
58
- onPress={() => this.setState({ error: false })}>
65
+ accessibilityRole={'button'}
66
+ onPress={() => this.setState({ error: false })}
67
+ >
59
68
  <Text>Retry</Text>
60
69
  </Pressable>
61
70
  </View>
@@ -80,58 +89,77 @@ export function AtomHost({
80
89
  onMeasure?: (event: import('react-native').LayoutChangeEvent) => void;
81
90
  nativeID?: string;
82
91
  }) {
83
- const [measurement, setMeasurement] = useState({ width, height: estimatedHeight });
84
- const [focused, setFocused] = useState(false);
85
- const [active, setActive] = useState(false);
86
- const [pending, setPending] = useState(0);
87
- const [updateError, setUpdateError] = useState<Error | null>(null);
92
+ const [ measurement, setMeasurement ] = useState({ width, height: estimatedHeight });
93
+ const [ focused, setFocused ] = useState(false);
94
+ const [ active, setActive ] = useState(false);
95
+ const [ pending, setPending ] = useState(0);
96
+ const [ updateError, setUpdateError ] = useState<Error | null>(null);
88
97
  const latestRequest = useRef(0);
89
98
  const applyUpdate = atomProps.updateAttrs;
90
99
  const mounted = useRef(true);
100
+
91
101
  useEffect(() => {
92
102
  mounted.current = true;
103
+
93
104
  return () => {
94
105
  mounted.current = false;
95
106
  };
96
107
  }, []);
108
+
97
109
  const updateAttrs = useCallback(
98
- async (update: AtomAttrsUpdate) => {
99
- if (!mounted.current)
110
+ async(update: AtomAttrsUpdate) => {
111
+ if (!mounted.current) {
100
112
  throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
113
+ }
114
+
101
115
  const request = ++latestRequest.current;
102
- setPending((count) => count + 1);
116
+ setPending(count => count + 1);
103
117
  setUpdateError(null);
118
+
104
119
  try {
105
120
  await applyUpdate(update);
106
121
  } catch (error) {
107
- if (mounted.current && latestRequest.current === request)
122
+ if (mounted.current && latestRequest.current === request) {
108
123
  setUpdateError(error instanceof Error ? error : new Error(String(error)));
124
+ }
125
+
109
126
  throw error;
110
127
  } finally {
111
- if (mounted.current) setPending((count) => count - 1);
128
+ if (mounted.current) {
129
+ setPending(count => count - 1);
130
+ }
112
131
  }
113
132
  },
114
- [applyUpdate]
133
+ [ applyUpdate ]
115
134
  );
135
+
116
136
  const editor = useMemo(() => {
117
- if (!atomProps.editor) return undefined;
118
- const guard = (action: () => Promise<void>) => async () => {
119
- if (!mounted.current)
137
+ if (!atomProps.editor) {
138
+ return undefined;
139
+ }
140
+
141
+ const guard = (action: () => Promise<void>) => async() => {
142
+ if (!mounted.current) {
120
143
  throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
144
+ }
145
+
121
146
  return action();
122
147
  };
148
+
123
149
  return {
124
150
  select: guard(atomProps.editor.select),
125
151
  delete: guard(atomProps.editor.delete),
126
152
  focusBefore: guard(atomProps.editor.focusBefore),
127
153
  focusAfter: guard(atomProps.editor.focusAfter),
128
154
  };
129
- }, [atomProps.editor]);
155
+ }, [ atomProps.editor ]);
156
+
130
157
  const retainedHeight = measurement.width === width ? measurement.height : estimatedHeight;
131
158
  const show = visible || focused || active || pending > 0 || atomProps.selected;
159
+
132
160
  return (
133
161
  <View
134
- testID='atom-host'
162
+ testID={'atom-host'}
135
163
  nativeID={nativeID}
136
164
  collapsable={false}
137
165
  pointerEvents={atomProps.interactive === false ? 'none' : 'box-none'}
@@ -139,23 +167,24 @@ export function AtomHost({
139
167
  importantForAccessibility={!show ? 'no-hide-descendants' : 'auto'}
140
168
  onFocus={() => setFocused(true)}
141
169
  onBlur={() => setFocused(false)}
142
- onLayout={(event) => {
170
+ onLayout={event => {
143
171
  onMeasure?.(event);
144
172
  const size = event.nativeEvent.layout;
173
+
145
174
  if (
146
175
  show &&
147
176
  Number.isFinite(size.height) &&
148
177
  size.height >= 0 &&
149
178
  Math.abs(size.width - width) <= 1
150
179
  ) {
151
- setMeasurement((previous) =>
180
+ setMeasurement(previous =>
152
181
  previous.width === width && previous.height === size.height
153
182
  ? previous
154
- : { width, height: size.height }
155
- );
183
+ : { width, height: size.height });
156
184
  }
157
185
  }}
158
- style={show ? undefined : { height: retainedHeight }}>
186
+ style={show ? undefined : { height: retainedHeight }}
187
+ >
159
188
  {show && (
160
189
  <AtomErrorBoundary component={Component} nodeType={atomProps.nodeType}>
161
190
  <Suspense
@@ -163,7 +192,8 @@ export function AtomHost({
163
192
  <View style={{ height: retainedHeight }}>
164
193
  <Text>Loading {atomProps.nodeType}…</Text>
165
194
  </View>
166
- }>
195
+ }
196
+ >
167
197
  <Component
168
198
  {...atomProps}
169
199
  editor={editor}
@@ -37,21 +37,31 @@ export type EditorAddonEntry = EditorAddon | false | null | undefined;
37
37
  export type EditorAddons = readonly EditorAddonEntry[];
38
38
 
39
39
  function copyConfiguration<T>(value: T, ancestors = new Set<object>()): T {
40
- if (value === null || typeof value !== 'object') return value;
41
- if (ancestors.has(value)) throw new Error('Addon configuration must not contain cycles.');
40
+ if (value === null || typeof value !== 'object') {
41
+ return value;
42
+ }
43
+
44
+ if (ancestors.has(value)) {
45
+ throw new Error('Addon configuration must not contain cycles.');
46
+ }
47
+
42
48
  if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) {
43
49
  throw new Error('Addon configuration must contain only plain objects and arrays.');
44
50
  }
51
+
45
52
  ancestors.add(value);
53
+
46
54
  const copy = Array.isArray(value)
47
- ? value.map((entry) => copyConfiguration(entry, ancestors))
55
+ ? value.map((entry: unknown) => copyConfiguration(entry, ancestors))
48
56
  : Object.fromEntries(
49
- Object.entries(value).map(([key, entry]) => [
50
- key,
51
- copyConfiguration(entry, ancestors),
52
- ])
53
- );
57
+ Object.entries(value).map(([ key, entry ]) => [
58
+ key,
59
+ copyConfiguration(entry, ancestors),
60
+ ])
61
+ );
62
+
54
63
  ancestors.delete(value);
64
+
55
65
  return Object.freeze(copy) as T;
56
66
  }
57
67
 
@@ -14,36 +14,65 @@ function isRecord(value: unknown): value is Record<string, unknown> {
14
14
  }
15
15
 
16
16
  export function normalizeEditorAddons(addons?: EditorAddons): NormalizedEditorAddons {
17
- if (addons === undefined) return {};
18
- if (!Array.isArray(addons)) throw new Error('addons must be a flat readonly array.');
17
+ if (addons === undefined) {
18
+ return {};
19
+ }
20
+
21
+ if (!Array.isArray(addons)) {
22
+ throw new Error('addons must be a flat readonly array.');
23
+ }
24
+
19
25
  const normalized: NormalizedEditorAddons = {};
20
26
  const ids = new Set<string>();
21
- for (const [index, addon] of addons.entries()) {
22
- if (addon === false || addon === null || addon === undefined) continue;
27
+
28
+ for (const [ index, addon ] of addons.entries()) {
29
+ if (addon === false || addon === null || addon === undefined) {
30
+ continue;
31
+ }
32
+
23
33
  const path = `addons[${index}]`;
24
- if (!isRecord(addon)) throw new Error(`${path} must be an addon descriptor.`);
25
- if (addon.version !== 1) throw new Error(`${path}.version must be 1.`);
34
+
35
+ if (!isRecord(addon)) {
36
+ throw new Error(`${path} must be an addon descriptor.`);
37
+ }
38
+
39
+ if (addon.version !== 1) {
40
+ throw new Error(`${path}.version must be 1.`);
41
+ }
42
+
26
43
  if (addon.capability !== 'mentions' && addon.capability !== 'code-highlighting') {
27
44
  throw new Error(`${path}.capability is unsupported.`);
28
45
  }
29
- if (addon.id !== addon.capability) throw new Error(`${path}.id must match its capability.`);
30
- if (ids.has(addon.capability))
31
- throw new Error(`${path}: duplicate addon id '${addon.id}'.`);
46
+
47
+ if (addon.id !== addon.capability) {
48
+ throw new Error(`${path}.id must match its capability.`);
49
+ }
50
+
51
+ if (ids.has(addon.capability)) {
52
+ throw new Error(`${path}: duplicate addon id '${String(addon.id)}'.`);
53
+ }
54
+
32
55
  ids.add(addon.capability);
33
- if (!isRecord(addon.options)) throw new Error(`${path}.options must be an object.`);
56
+
57
+ if (!isRecord(addon.options)) {
58
+ throw new Error(`${path}.options must be an object.`);
59
+ }
60
+
34
61
  if (addon.capability === 'mentions') {
35
62
  normalized.mentions = addon.options;
36
63
  } else {
37
- for (const key of ['provider', 'theme'] as const) {
64
+ for (const key of [ 'provider', 'theme' ] as const) {
38
65
  if (typeof addon.options[key] !== 'string' || !addon.options[key].trim()) {
39
66
  throw new Error(`${path}.options.${key} must be a nonempty string.`);
40
67
  }
41
68
  }
69
+
42
70
  normalized.codeHighlighting = {
43
71
  provider: addon.options.provider as string,
44
72
  theme: addon.options.theme as string,
45
73
  };
46
74
  }
47
75
  }
76
+
48
77
  return normalized;
49
78
  }