@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,27 @@
1
+ package com.openeditor.editor
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
+ import org.junit.Test
6
+
7
+ class NativeEditorModuleTest {
8
+ @Test
9
+ fun `native unsigned argument helpers reject negative values`() {
10
+ assertNull(nativeULong(-1))
11
+ assertNull(nativeUInt(-1))
12
+ }
13
+
14
+ @Test
15
+ fun `native unsigned argument helpers keep non-negative values`() {
16
+ assertEquals(0UL, nativeULong(0))
17
+ assertEquals(42UL, nativeULong(42))
18
+ assertEquals(0U, nativeUInt(0))
19
+ assertEquals(42U, nativeUInt(42))
20
+ }
21
+
22
+ @Test
23
+ fun `native argument error returns bridge parseable json`() {
24
+ assertEquals("{\"error\":\"invalid editor id\"}", nativeArgumentError("editor id"))
25
+ assertEquals("{\"error\":\"invalid position\"}", nativeArgumentError("position"))
26
+ }
27
+ }
@@ -0,0 +1,197 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import org.junit.Assert.assertEquals
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertNotNull
11
+ import org.junit.Assert.assertTrue
12
+ import org.junit.Test
13
+ import org.junit.runner.RunWith
14
+ import org.robolectric.RobolectricTestRunner
15
+ import org.robolectric.RuntimeEnvironment
16
+ import org.robolectric.annotation.Config
17
+
18
+ @RunWith(RobolectricTestRunner::class)
19
+ @Config(sdk = [34])
20
+ class NativePerformanceTest {
21
+ private val context = RuntimeEnvironment.getApplication()
22
+ private val baseFontSize = 16f
23
+ private val textColor = Color.BLACK
24
+
25
+ @Test
26
+ fun `performance - render bridge large document`() {
27
+ val renderJson = NativePerformanceFixtureFactory.largeRenderJson()
28
+
29
+ val stats = measureOperation("renderBridgeLargeDocument") {
30
+ val spannable = RenderBridge.buildSpannable(
31
+ renderJson,
32
+ baseFontSize,
33
+ textColor,
34
+ density = context.resources.displayMetrics.density
35
+ )
36
+
37
+ assertTrue("rendered spannable should not be empty", spannable.isNotEmpty())
38
+ if (spannable.isNotEmpty()) {
39
+ spannable.getSpans(
40
+ 0,
41
+ minOf(spannable.length, 1),
42
+ Any::class.java
43
+ )
44
+ }
45
+ }
46
+
47
+ assertTrue("average render time should be positive", stats.averageMillis > 0.0)
48
+ }
49
+
50
+ @Test
51
+ fun `performance - apply update json large document`() {
52
+ val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
53
+ val editText = EditorEditText(context).apply {
54
+ layoutParams = ViewGroup.LayoutParams(
55
+ ViewGroup.LayoutParams.MATCH_PARENT,
56
+ ViewGroup.LayoutParams.WRAP_CONTENT
57
+ )
58
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
59
+ captureApplyUpdateTraceForTesting = true
60
+ }
61
+ val traceSamples = mutableListOf<EditorEditText.ApplyUpdateTrace>()
62
+
63
+ editText.applyUpdateJSON(updateJson, notifyListener = false)
64
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
65
+
66
+ val stats = measureOperation("applyUpdateJsonLargeDocument") {
67
+ editText.applyUpdateJSON(updateJson, notifyListener = false)
68
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
69
+ editText.lastApplyUpdateTrace()?.let(traceSamples::add)
70
+
71
+ assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
72
+ assertNotNull("edit text should have a layout after applying update", editText.layout)
73
+ }
74
+ println(
75
+ ApplyUpdateTraceStats(
76
+ name = "applyUpdateJsonLargeDocument.breakdown",
77
+ traces = traceSamples.takeLast(5)
78
+ ).summaryString()
79
+ )
80
+
81
+ assertTrue("average applyUpdateJSON time should be positive", stats.averageMillis > 0.0)
82
+ }
83
+
84
+ @Test
85
+ fun `performance - apply render patch large document`() {
86
+ val initialUpdateJson = NativePerformanceFixtureFactory.largeUpdateJson()
87
+ val patchedUpdateJson = NativePerformanceFixtureFactory.largePatchedUpdateJson()
88
+ val editText = EditorEditText(context).apply {
89
+ layoutParams = ViewGroup.LayoutParams(
90
+ ViewGroup.LayoutParams.MATCH_PARENT,
91
+ ViewGroup.LayoutParams.WRAP_CONTENT
92
+ )
93
+ setBaseStyle(baseFontSize, textColor, Color.WHITE)
94
+ captureApplyUpdateTraceForTesting = true
95
+ }
96
+ val traceSamples = mutableListOf<EditorEditText.ApplyUpdateTrace>()
97
+
98
+ val stats = measureOperation(
99
+ name = "applyRenderPatchLargeDocument",
100
+ beforeEach = {
101
+ editText.applyUpdateJSON(initialUpdateJson, notifyListener = false)
102
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
103
+ }
104
+ ) {
105
+ editText.applyUpdateJSON(patchedUpdateJson, notifyListener = false)
106
+ layoutView(editText, widthPx = 1080, heightPx = 2400, heightMode = View.MeasureSpec.AT_MOST)
107
+ editText.lastApplyUpdateTrace()?.let(traceSamples::add)
108
+
109
+ assertFalse("edit text should contain rendered content", editText.text.isNullOrEmpty())
110
+ }
111
+ println(
112
+ ApplyUpdateTraceStats(
113
+ name = "applyRenderPatchLargeDocument.breakdown",
114
+ traces = traceSamples.takeLast(5)
115
+ ).summaryString()
116
+ )
117
+
118
+ assertTrue("average patch apply time should be positive", stats.averageMillis > 0.0)
119
+ }
120
+
121
+ @Test
122
+ fun `performance - remote selection overlay refresh multi peer large document`() {
123
+ val updateJson = NativePerformanceFixtureFactory.largeUpdateJson()
124
+ val richTextView = RichTextEditorView(context).apply {
125
+ configure(textSizePx = baseFontSize, textColor = textColor, backgroundColor = Color.WHITE)
126
+ setRemoteSelectionEditorIdForTesting(1L)
127
+ setRemoteSelectionScalarResolverForTesting { _, docPos -> docPos }
128
+ }
129
+
130
+ richTextView.editorEditText.applyUpdateJSON(updateJson, notifyListener = false)
131
+ layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
132
+
133
+ val totalScalar = richTextView.editorEditText.text?.length ?: 0
134
+ val selections = NativePerformanceFixtureFactory.remoteSelections(
135
+ totalScalar,
136
+ peerCount = 24,
137
+ selectionWidth = 24
138
+ )
139
+ richTextView.setRemoteSelections(selections)
140
+ layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
141
+
142
+ val bitmap = Bitmap.createBitmap(1080, 1600, Bitmap.Config.ARGB_8888)
143
+ val canvas = Canvas(bitmap)
144
+
145
+ val stats = measureOperation("remoteSelectionOverlayRefreshMultiPeerLargeDocument") {
146
+ bitmap.eraseColor(Color.TRANSPARENT)
147
+ richTextView.setRemoteSelections(selections)
148
+ layoutView(richTextView, widthPx = 1080, heightPx = 1600, heightMode = View.MeasureSpec.EXACTLY)
149
+ richTextView.draw(canvas)
150
+
151
+ val snapshots = richTextView.remoteSelectionDebugSnapshotsForTesting()
152
+ assertTrue(
153
+ "remote selection overlay should resolve visible carets",
154
+ snapshots.any { it.caretRect != null }
155
+ )
156
+ assertEquals("expected one snapshot per peer", selections.size, snapshots.size)
157
+ }
158
+
159
+ assertTrue("average remote selection refresh time should be positive", stats.averageMillis > 0.0)
160
+ }
161
+
162
+ private fun layoutView(
163
+ view: View,
164
+ widthPx: Int,
165
+ heightPx: Int,
166
+ heightMode: Int
167
+ ) {
168
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(widthPx, View.MeasureSpec.EXACTLY)
169
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(heightPx, heightMode)
170
+ view.measure(widthSpec, heightSpec)
171
+ view.layout(0, 0, view.measuredWidth, view.measuredHeight)
172
+ }
173
+
174
+ private fun measureOperation(
175
+ name: String,
176
+ warmupIterations: Int = 3,
177
+ measuredIterations: Int = 5,
178
+ beforeEach: (() -> Unit)? = null,
179
+ block: () -> Unit
180
+ ): TimingStats {
181
+ repeat(warmupIterations) {
182
+ beforeEach?.invoke()
183
+ block()
184
+ }
185
+
186
+ val samples = MutableList(measuredIterations) {
187
+ beforeEach?.invoke()
188
+ val startedAt = System.nanoTime()
189
+ block()
190
+ System.nanoTime() - startedAt
191
+ }
192
+
193
+ val stats = TimingStats(name, samples)
194
+ println(stats.summaryString())
195
+ return stats
196
+ }
197
+ }
@@ -0,0 +1,84 @@
1
+ package com.openeditor.editor
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import expo.modules.core.ModuleRegistry
6
+ import expo.modules.kotlin.AppContext
7
+ import expo.modules.kotlin.ModulesProvider
8
+ import expo.modules.kotlin.modules.Module
9
+ import org.junit.Assert.assertEquals
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+ import org.junit.runner.RunWith
13
+ import org.robolectric.RobolectricTestRunner
14
+ import org.robolectric.RuntimeEnvironment
15
+ import org.robolectric.annotation.Config
16
+ import java.lang.ref.WeakReference
17
+
18
+ @RunWith(RobolectricTestRunner::class)
19
+ @Config(sdk = [34])
20
+ class NativeProseViewerExpoViewTest {
21
+ @Test
22
+ fun `viewer measure ignores stale exact parent height`() {
23
+ val expoContext = testExpoContext(RuntimeEnvironment.getApplication())
24
+ val view = NativeProseViewerExpoView(expoContext.context, expoContext.appContext)
25
+ view.suppressContentHeightEventsForTesting = true
26
+ view.setRenderJson(paragraphRenderJson(LONG_MESSAGE_TEXT))
27
+
28
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
29
+ val wrapHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
30
+ view.measure(widthSpec, wrapHeightSpec)
31
+ view.layout(0, 0, view.measuredWidth, view.measuredHeight)
32
+ val contentHeight = view.measuredHeight
33
+
34
+ assertTrue(contentHeight > 0)
35
+
36
+ val staleExactHeightSpec = View.MeasureSpec.makeMeasureSpec(
37
+ contentHeight + 480,
38
+ View.MeasureSpec.EXACTLY
39
+ )
40
+ view.measure(widthSpec, staleExactHeightSpec)
41
+
42
+ assertEquals(contentHeight, view.measuredHeight)
43
+ }
44
+
45
+ private data class TestExpoContext(
46
+ val context: Context,
47
+ val appContext: AppContext
48
+ )
49
+
50
+ private fun testExpoContext(context: Context): TestExpoContext {
51
+ val reactContext = Class
52
+ .forName("com.facebook.react.bridge.BridgeReactContext")
53
+ .getConstructor(Context::class.java)
54
+ .newInstance(context) as Context
55
+
56
+ val modulesProvider = object : ModulesProvider {
57
+ override fun getModulesList(): List<Class<out Module>> = emptyList()
58
+ }
59
+ val constructor = AppContext::class.java.constructors.first { constructor ->
60
+ constructor.parameterTypes.size == 3
61
+ }
62
+ val appContext = constructor.newInstance(
63
+ modulesProvider,
64
+ ModuleRegistry(emptyList(), emptyList()),
65
+ WeakReference(reactContext)
66
+ ) as AppContext
67
+ return TestExpoContext(reactContext, appContext)
68
+ }
69
+
70
+ private fun paragraphRenderJson(text: String): String =
71
+ """
72
+ [
73
+ {"type":"blockStart","nodeType":"paragraph","depth":0},
74
+ {"type":"textRun","text":"$text","marks":[]},
75
+ {"type":"blockEnd"}
76
+ ]
77
+ """.trimIndent()
78
+
79
+ private companion object {
80
+ private val LONG_MESSAGE_TEXT = List(80) {
81
+ "Long Android viewer message"
82
+ }.joinToString(" ")
83
+ }
84
+ }