@openeditor/react-native-prose-editor 0.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 (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,350 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.app.Instrumentation
4
+ import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import android.graphics.Canvas
7
+ import android.graphics.Color
8
+ import android.os.Bundle
9
+ import android.view.View
10
+ import android.view.ViewGroup
11
+ import androidx.test.core.app.ApplicationProvider
12
+ import androidx.test.ext.junit.runners.AndroidJUnit4
13
+ import androidx.test.filters.LargeTest
14
+ import androidx.test.platform.app.InstrumentationRegistry
15
+ import java.util.concurrent.atomic.AtomicReference
16
+ import org.junit.Assert.assertEquals
17
+ import org.junit.Assert.assertFalse
18
+ import org.junit.Assert.assertNotNull
19
+ import org.junit.Assert.assertTrue
20
+ import org.junit.Test
21
+ import org.junit.runner.RunWith
22
+ import uniffi.editor_core.*
23
+
24
+ @RunWith(AndroidJUnit4::class)
25
+ @LargeTest
26
+ class NativeDevicePerformanceTest {
27
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
28
+ private val context = ApplicationProvider.getApplicationContext<Context>()
29
+ private val baseFontSize = 16f
30
+ private val textColor = Color.BLACK
31
+
32
+ @Test
33
+ fun performance_renderBridgeLargeDocument() {
34
+ val renderJson = NativePerformanceFixtureFactory.largeRenderJson()
35
+
36
+ val stats = measureOperation("renderBridgeLargeDocument") {
37
+ val spannable = runOnMainSyncWithResult {
38
+ RenderBridge.buildSpannable(
39
+ renderJson,
40
+ baseFontSize,
41
+ textColor,
42
+ density = context.resources.displayMetrics.density
43
+ )
44
+ }
45
+
46
+ assertTrue("rendered spannable should not be empty", spannable.isNotEmpty())
47
+ if (spannable.isNotEmpty()) {
48
+ runOnMainSyncWithResult {
49
+ spannable.getSpans(0, minOf(spannable.length, 1), Any::class.java)
50
+ }
51
+ }
52
+ }
53
+
54
+ reportStats(stats)
55
+ assertTrue("average render time should be positive", stats.averageMillis > 0.0)
56
+ }
57
+
58
+ @Test
59
+ fun performance_applyUpdateJsonLargeDocument() {
60
+ val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
61
+ val editText = runOnMainSyncWithResult {
62
+ EditorEditText(context).apply {
63
+ layoutParams = ViewGroup.LayoutParams(
64
+ ViewGroup.LayoutParams.MATCH_PARENT,
65
+ ViewGroup.LayoutParams.WRAP_CONTENT
66
+ )
67
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
68
+ applyUpdateJSON(updateJson, notifyListener = false)
69
+ layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
70
+ }
71
+ }
72
+
73
+ val stats = measureOperation("applyUpdateJsonLargeDocument") {
74
+ runOnMainSyncWithResult {
75
+ editText.applyUpdateJSON(updateJson, notifyListener = false)
76
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
77
+ }
78
+
79
+ assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
80
+ assertNotNull("edit text should have a layout after applying update", editText.layout)
81
+ }
82
+
83
+ reportStats(stats)
84
+ assertTrue("average applyUpdateJSON time should be positive", stats.averageMillis > 0.0)
85
+ }
86
+
87
+ @Test
88
+ fun performance_applyRenderPatchLargeDocument() {
89
+ val initialUpdateJson = NativePerformanceFixtureFactory.largeUpdateJson()
90
+ val patchedUpdateJson = NativePerformanceFixtureFactory.largePatchedUpdateJson()
91
+ val editText = runOnMainSyncWithResult {
92
+ EditorEditText(context).apply {
93
+ layoutParams = ViewGroup.LayoutParams(
94
+ ViewGroup.LayoutParams.MATCH_PARENT,
95
+ ViewGroup.LayoutParams.WRAP_CONTENT
96
+ )
97
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
98
+ }
99
+ }
100
+
101
+ val stats = measureOperation(
102
+ name = "applyRenderPatchLargeDocument",
103
+ beforeEach = {
104
+ runOnMainSyncWithResult {
105
+ editText.applyUpdateJSON(initialUpdateJson, notifyListener = false)
106
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
107
+ }
108
+ }
109
+ ) {
110
+ runOnMainSyncWithResult {
111
+ editText.applyUpdateJSON(patchedUpdateJson, notifyListener = false)
112
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
113
+ }
114
+
115
+ assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
116
+ }
117
+
118
+ reportStats(stats)
119
+ assertTrue("average patch apply time should be positive", stats.averageMillis > 0.0)
120
+ }
121
+
122
+ @Test
123
+ fun performance_typingRoundTripLargeDocument() {
124
+ val editorId = editorCreate("{}").toLong()
125
+ try {
126
+ NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
127
+ val editText = runOnMainSyncWithResult {
128
+ EditorEditText(context).apply {
129
+ layoutParams = ViewGroup.LayoutParams(
130
+ ViewGroup.LayoutParams.MATCH_PARENT,
131
+ ViewGroup.LayoutParams.WRAP_CONTENT
132
+ )
133
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
134
+ bindEditor(editorId)
135
+ layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
136
+ }
137
+ }
138
+ val typingOffset = NativePerformanceFixtureFactory.typingCursorOffset(editText.text ?: "")
139
+
140
+ val stats = measureOperation("typingRoundTripLargeDocument") {
141
+ runOnMainSyncWithResult {
142
+ editText.setSelection(typingOffset)
143
+ editText.handleTextCommit("!")
144
+ editText.handleDelete(1, 0)
145
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
146
+ }
147
+
148
+ assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
149
+ assertNotNull("edit text should have a layout after typing", editText.layout)
150
+ }
151
+
152
+ reportStats(stats)
153
+ assertTrue("average typing time should be positive", stats.averageMillis > 0.0)
154
+ } finally {
155
+ editorDestroy(editorId.toULong())
156
+ }
157
+ }
158
+
159
+ @Test
160
+ fun performance_paragraphSplitRoundTripLargeDocument() {
161
+ val editorId = editorCreate("{}").toLong()
162
+ try {
163
+ val largeDocumentJson = NativePerformanceFixtureFactory.largeDocumentJson()
164
+ NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
165
+ val editText = runOnMainSyncWithResult {
166
+ EditorEditText(context).apply {
167
+ layoutParams = ViewGroup.LayoutParams(
168
+ ViewGroup.LayoutParams.MATCH_PARENT,
169
+ ViewGroup.LayoutParams.WRAP_CONTENT
170
+ )
171
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
172
+ bindEditor(editorId)
173
+ layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
174
+ }
175
+ }
176
+ val splitOffset = NativePerformanceFixtureFactory.typingCursorOffset(editText.text ?: "")
177
+
178
+ val stats = measureOperation(
179
+ name = "paragraphSplitRoundTripLargeDocument",
180
+ beforeEach = {
181
+ runOnMainSyncWithResult {
182
+ editorSetJson(editorId.toULong(), largeDocumentJson)
183
+ editText.applyUpdateJSON(editorGetCurrentState(editorId.toULong()), notifyListener = false)
184
+ editText.setSelection(splitOffset)
185
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
186
+ }
187
+ }
188
+ ) {
189
+ runOnMainSyncWithResult {
190
+ editText.handleTextCommit("\n")
191
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
192
+ }
193
+
194
+ assertNotNull("edit text should have a layout after paragraph split", editText.layout)
195
+ }
196
+
197
+ reportStats(stats)
198
+ assertTrue("average paragraph split time should be positive", stats.averageMillis > 0.0)
199
+ } finally {
200
+ editorDestroy(editorId.toULong())
201
+ }
202
+ }
203
+
204
+ @Test
205
+ fun performance_selectionScrubLargeDocument() {
206
+ val editorId = editorCreate("{}").toLong()
207
+ try {
208
+ NativePerformanceFixtureFactory.loadLargeDocumentIntoEditor(editorId.toULong())
209
+ val editText = runOnMainSyncWithResult {
210
+ EditorEditText(context).apply {
211
+ layoutParams = ViewGroup.LayoutParams(
212
+ ViewGroup.LayoutParams.MATCH_PARENT,
213
+ ViewGroup.LayoutParams.WRAP_CONTENT
214
+ )
215
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
216
+ bindEditor(editorId)
217
+ layoutView(this, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
218
+ }
219
+ }
220
+ val scrubOffsets = NativePerformanceFixtureFactory.selectionScrubOffsets(
221
+ editText.text ?: "",
222
+ points = 48
223
+ )
224
+
225
+ val stats = measureOperation("selectionScrubLargeDocument") {
226
+ runOnMainSyncWithResult {
227
+ for (offset in scrubOffsets) {
228
+ editText.setSelection(offset)
229
+ }
230
+ }
231
+
232
+ assertEquals(
233
+ "selection should land on the final scrub offset",
234
+ scrubOffsets.last(),
235
+ editText.selectionStart
236
+ )
237
+ }
238
+
239
+ reportStats(stats)
240
+ assertTrue("average selection scrub time should be positive", stats.averageMillis > 0.0)
241
+ } finally {
242
+ editorDestroy(editorId.toULong())
243
+ }
244
+ }
245
+
246
+ @Test
247
+ fun performance_remoteSelectionOverlayRefreshMultiPeerLargeDocument() {
248
+ val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
249
+ val richTextView = runOnMainSyncWithResult {
250
+ RichTextEditorView(context).apply {
251
+ configure(textSizePx = baseFontSize, textColor = textColor, backgroundColor = Color.WHITE)
252
+ setRemoteSelectionEditorIdForTesting(1L)
253
+ setRemoteSelectionScalarResolverForTesting { _, docPos -> docPos }
254
+ editorEditText.applyUpdateJSON(updateJson, notifyListener = false)
255
+ layoutView(this, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
256
+ }
257
+ }
258
+
259
+ val totalScalar = richTextView.editorEditText.text?.length ?: 0
260
+ val selections = NativePerformanceFixtureFactory.remoteSelections(
261
+ totalScalar,
262
+ peerCount = 24,
263
+ selectionWidth = 24
264
+ )
265
+ runOnMainSyncWithResult {
266
+ richTextView.setRemoteSelections(selections)
267
+ layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
268
+ }
269
+
270
+ val bitmap = Bitmap.createBitmap(1080, 1600, Bitmap.Config.ARGB_8888)
271
+ val canvas = Canvas(bitmap)
272
+
273
+ val stats = measureOperation("remoteSelectionOverlayRefreshMultiPeerLargeDocument") {
274
+ val hasVisibleCaret = runOnMainSyncWithResult {
275
+ bitmap.eraseColor(Color.TRANSPARENT)
276
+ richTextView.setRemoteSelections(selections)
277
+ layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
278
+ richTextView.draw(canvas)
279
+ richTextView.remoteSelectionDebugSnapshotsForTesting().any { it.caretRect != null }
280
+ }
281
+
282
+ assertTrue("remote selection overlay should resolve visible carets", hasVisibleCaret)
283
+ assertEquals("expected one snapshot per peer", selections.size, richTextView.remoteSelectionDebugSnapshotsForTesting().size)
284
+ }
285
+
286
+ reportStats(stats)
287
+ assertTrue("average remote selection refresh time should be positive", stats.averageMillis > 0.0)
288
+ }
289
+
290
+ private fun layoutView(
291
+ view: View,
292
+ widthPx: Int,
293
+ heightPx: Int,
294
+ heightMode: Int
295
+ ) {
296
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY)
297
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(heightPx, heightMode)
298
+ view.measure(widthSpec, heightSpec)
299
+ view.layout(0, 0, view.measuredWidth, view.measuredHeight)
300
+ }
301
+
302
+ private fun measureOperation(
303
+ name: String,
304
+ warmupIterations: Int = 3,
305
+ measuredIterations: Int = 5,
306
+ beforeEach: (() -> Unit)? = null,
307
+ block: () -> Unit
308
+ ): TimingStats {
309
+ repeat(warmupIterations) {
310
+ beforeEach?.invoke()
311
+ block()
312
+ }
313
+
314
+ val samples = MutableList(measuredIterations) {
315
+ beforeEach?.invoke()
316
+ val startedAt = System.nanoTime()
317
+ block()
318
+ System.nanoTime() - startedAt
319
+ }
320
+
321
+ return TimingStats(name, samples)
322
+ }
323
+
324
+ private fun reportStats(stats: TimingStats) {
325
+ instrumentation.sendStatus(
326
+ 0,
327
+ Bundle().apply {
328
+ putString(
329
+ Instrumentation.REPORT_KEY_STREAMRESULT,
330
+ stats.summaryString(tag = "NativeDevicePerformanceTest") + "\n"
331
+ )
332
+ }
333
+ )
334
+ }
335
+
336
+ @Suppress("UNCHECKED_CAST")
337
+ private fun <T> runOnMainSyncWithResult(block: () -> T): T {
338
+ val result = AtomicReference<Any?>()
339
+ val error = AtomicReference<Throwable?>()
340
+ instrumentation.runOnMainSync {
341
+ try {
342
+ result.set(block())
343
+ } catch (throwable: Throwable) {
344
+ error.set(throwable)
345
+ }
346
+ }
347
+ error.get()?.let { throw it }
348
+ return result.get() as T
349
+ }
350
+ }
@@ -0,0 +1,5 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.app.Activity
4
+
5
+ class NativeEditorOutsideTapActivity : Activity()