@apollohg/react-native-prose-editor 0.5.15 → 0.5.16
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.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +74 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/RichTextEditorView.swift +112 -2
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -18,6 +18,7 @@ import android.util.Log
|
|
|
18
18
|
import android.util.TypedValue
|
|
19
19
|
import android.view.KeyEvent
|
|
20
20
|
import android.view.MotionEvent
|
|
21
|
+
import android.view.inputmethod.BaseInputConnection
|
|
21
22
|
import android.view.inputmethod.EditorInfo
|
|
22
23
|
import android.view.inputmethod.InputConnection
|
|
23
24
|
import android.view.inputmethod.InputMethodManager
|
|
@@ -107,6 +108,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
107
108
|
val end: Int
|
|
108
109
|
)
|
|
109
110
|
|
|
111
|
+
private data class NativeTextMutation(
|
|
112
|
+
val scalarFrom: Int,
|
|
113
|
+
val scalarTo: Int,
|
|
114
|
+
val replacementText: String,
|
|
115
|
+
val resultingText: String
|
|
116
|
+
)
|
|
117
|
+
|
|
110
118
|
/**
|
|
111
119
|
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
112
120
|
*/
|
|
@@ -1223,6 +1231,66 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1223
1231
|
applyUpdateJSON(updateJSON)
|
|
1224
1232
|
}
|
|
1225
1233
|
|
|
1234
|
+
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
1235
|
+
val authorizedText = lastAuthorizedText
|
|
1236
|
+
if (currentText == authorizedText) return null
|
|
1237
|
+
|
|
1238
|
+
var prefix = 0
|
|
1239
|
+
val sharedLength = minOf(authorizedText.length, currentText.length)
|
|
1240
|
+
while (
|
|
1241
|
+
prefix < sharedLength &&
|
|
1242
|
+
authorizedText[prefix] == currentText[prefix]
|
|
1243
|
+
) {
|
|
1244
|
+
prefix++
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
var authorizedEnd = authorizedText.length
|
|
1248
|
+
var currentEnd = currentText.length
|
|
1249
|
+
while (
|
|
1250
|
+
authorizedEnd > prefix &&
|
|
1251
|
+
currentEnd > prefix &&
|
|
1252
|
+
authorizedText[authorizedEnd - 1] == currentText[currentEnd - 1]
|
|
1253
|
+
) {
|
|
1254
|
+
authorizedEnd--
|
|
1255
|
+
currentEnd--
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
val replacementText = currentText.substring(prefix, currentEnd)
|
|
1259
|
+
return NativeTextMutation(
|
|
1260
|
+
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
1261
|
+
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
1262
|
+
replacementText = replacementText,
|
|
1263
|
+
resultingText = currentText
|
|
1264
|
+
)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
private fun shouldAdoptNativeTextMutation(editable: Editable?): Boolean {
|
|
1268
|
+
if (!isEditable || !hasFocus()) return false
|
|
1269
|
+
if (editable == null) return true
|
|
1270
|
+
|
|
1271
|
+
val composingStart = BaseInputConnection.getComposingSpanStart(editable)
|
|
1272
|
+
val composingEnd = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1273
|
+
return composingStart < 0 || composingEnd < 0 || composingStart == composingEnd
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
1277
|
+
if ((text?.toString() ?: "") != mutation.resultingText) return
|
|
1278
|
+
|
|
1279
|
+
if (mutation.scalarFrom == mutation.scalarTo) {
|
|
1280
|
+
if (mutation.replacementText.isNotEmpty()) {
|
|
1281
|
+
insertTextInRust(mutation.replacementText, mutation.scalarFrom)
|
|
1282
|
+
}
|
|
1283
|
+
} else if (mutation.replacementText.isEmpty()) {
|
|
1284
|
+
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
1285
|
+
} else {
|
|
1286
|
+
replaceTextRangeInRust(
|
|
1287
|
+
mutation.scalarFrom,
|
|
1288
|
+
mutation.scalarTo,
|
|
1289
|
+
mutation.replacementText
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1226
1294
|
/**
|
|
1227
1295
|
* Delete a scalar range via the Rust editor.
|
|
1228
1296
|
*
|
|
@@ -2018,6 +2086,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2018
2086
|
val currentText = s?.toString() ?: ""
|
|
2019
2087
|
if (currentText == lastAuthorizedText) return
|
|
2020
2088
|
|
|
2089
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
2090
|
+
if (mutation != null && shouldAdoptNativeTextMutation(s)) {
|
|
2091
|
+
commitNativeTextMutation(mutation)
|
|
2092
|
+
return
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2021
2095
|
// Text has diverged from Rust's authorized state.
|
|
2022
2096
|
reconciliationCount++
|
|
2023
2097
|
Log.w(
|
|
@@ -8,32 +8,32 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>libeditor_core.a</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-
|
|
11
|
+
<string>ios-arm64</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>
|
|
18
17
|
</array>
|
|
19
18
|
<key>SupportedPlatform</key>
|
|
20
19
|
<string>ios</string>
|
|
21
|
-
<key>SupportedPlatformVariant</key>
|
|
22
|
-
<string>simulator</string>
|
|
23
20
|
</dict>
|
|
24
21
|
<dict>
|
|
25
22
|
<key>BinaryPath</key>
|
|
26
23
|
<string>libeditor_core.a</string>
|
|
27
24
|
<key>LibraryIdentifier</key>
|
|
28
|
-
<string>ios-
|
|
25
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
29
26
|
<key>LibraryPath</key>
|
|
30
27
|
<string>libeditor_core.a</string>
|
|
31
28
|
<key>SupportedArchitectures</key>
|
|
32
29
|
<array>
|
|
33
30
|
<string>arm64</string>
|
|
31
|
+
<string>x86_64</string>
|
|
34
32
|
</array>
|
|
35
33
|
<key>SupportedPlatform</key>
|
|
36
34
|
<string>ios</string>
|
|
35
|
+
<key>SupportedPlatformVariant</key>
|
|
36
|
+
<string>simulator</string>
|
|
37
37
|
</dict>
|
|
38
38
|
</array>
|
|
39
39
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
Binary file
|
|
@@ -819,6 +819,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
819
819
|
let entries: [TopLevelChildMetadata]
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
private struct NativeTextMutation {
|
|
823
|
+
let from: UInt32
|
|
824
|
+
let to: UInt32
|
|
825
|
+
let replacementText: String
|
|
826
|
+
let resultingText: String
|
|
827
|
+
}
|
|
828
|
+
|
|
822
829
|
private enum PositionCacheUpdate {
|
|
823
830
|
case scan
|
|
824
831
|
case invalidate
|
|
@@ -964,6 +971,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
964
971
|
/// trailing UIKit text-storage callbacks that arrive on the next run loop.
|
|
965
972
|
private var interceptedInputDepth = 0
|
|
966
973
|
private var reconciliationWorkScheduled = false
|
|
974
|
+
private var nativeTextMutationCommitScheduled = false
|
|
975
|
+
private var pendingNativeTextMutation: NativeTextMutation?
|
|
967
976
|
|
|
968
977
|
/// Coalesces selection sync until UIKit has finished resolving the
|
|
969
978
|
/// current tap/drag gesture's final caret position.
|
|
@@ -2209,7 +2218,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2209
2218
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
2210
2219
|
guard textView === self else { return }
|
|
2211
2220
|
ensureInternalTextViewDelegate()
|
|
2212
|
-
guard !isApplyingRustState, !isComposing else { return }
|
|
2221
|
+
guard !isApplyingRustState, !isComposing, !nativeTextMutationCommitScheduled else { return }
|
|
2213
2222
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
2214
2223
|
return
|
|
2215
2224
|
}
|
|
@@ -2390,7 +2399,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2390
2399
|
}
|
|
2391
2400
|
|
|
2392
2401
|
private func syncSelectionToRustAndNotifyDelegate() {
|
|
2393
|
-
guard !isApplyingRustState,
|
|
2402
|
+
guard !isApplyingRustState,
|
|
2403
|
+
!isComposing,
|
|
2404
|
+
!nativeTextMutationCommitScheduled,
|
|
2405
|
+
editorId != 0
|
|
2406
|
+
else {
|
|
2407
|
+
return
|
|
2408
|
+
}
|
|
2394
2409
|
guard let range = selectedTextRange else { return }
|
|
2395
2410
|
|
|
2396
2411
|
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
@@ -2997,6 +3012,94 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2997
3012
|
applyUpdateJSON(updateJSON)
|
|
2998
3013
|
}
|
|
2999
3014
|
|
|
3015
|
+
private func nativeTextMutationFromAuthorizedDiff(
|
|
3016
|
+
currentText: String
|
|
3017
|
+
) -> NativeTextMutation? {
|
|
3018
|
+
let authorizedText = lastAuthorizedText
|
|
3019
|
+
guard currentText != authorizedText else { return nil }
|
|
3020
|
+
|
|
3021
|
+
let authorized = authorizedText as NSString
|
|
3022
|
+
let current = currentText as NSString
|
|
3023
|
+
let sharedLength = min(authorized.length, current.length)
|
|
3024
|
+
var prefix = 0
|
|
3025
|
+
while prefix < sharedLength,
|
|
3026
|
+
authorized.character(at: prefix) == current.character(at: prefix) {
|
|
3027
|
+
prefix += 1
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
var authorizedEnd = authorized.length
|
|
3031
|
+
var currentEnd = current.length
|
|
3032
|
+
while authorizedEnd > prefix,
|
|
3033
|
+
currentEnd > prefix,
|
|
3034
|
+
authorized.character(at: authorizedEnd - 1) == current.character(at: currentEnd - 1) {
|
|
3035
|
+
authorizedEnd -= 1
|
|
3036
|
+
currentEnd -= 1
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
let replacementLength = currentEnd - prefix
|
|
3040
|
+
guard replacementLength >= 0 else { return nil }
|
|
3041
|
+
let replacementText = current.substring(
|
|
3042
|
+
with: NSRange(location: prefix, length: replacementLength)
|
|
3043
|
+
)
|
|
3044
|
+
|
|
3045
|
+
return NativeTextMutation(
|
|
3046
|
+
from: PositionBridge.utf16OffsetToScalar(prefix, in: authorizedText),
|
|
3047
|
+
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: authorizedText),
|
|
3048
|
+
replacementText: replacementText,
|
|
3049
|
+
resultingText: currentText
|
|
3050
|
+
)
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
private func shouldAdoptNativeTextStorageMutation() -> Bool {
|
|
3054
|
+
isFirstResponder && isEditable
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
|
|
3058
|
+
pendingNativeTextMutation = mutation
|
|
3059
|
+
guard !nativeTextMutationCommitScheduled else { return }
|
|
3060
|
+
|
|
3061
|
+
nativeTextMutationCommitScheduled = true
|
|
3062
|
+
DispatchQueue.main.async { [weak self] in
|
|
3063
|
+
guard let self else { return }
|
|
3064
|
+
self.nativeTextMutationCommitScheduled = false
|
|
3065
|
+
guard let mutation = self.pendingNativeTextMutation else { return }
|
|
3066
|
+
self.pendingNativeTextMutation = nil
|
|
3067
|
+
|
|
3068
|
+
guard self.editorId != 0,
|
|
3069
|
+
!self.isApplyingRustState,
|
|
3070
|
+
!self.isInterceptingInput,
|
|
3071
|
+
!self.isComposing,
|
|
3072
|
+
self.shouldAdoptNativeTextStorageMutation()
|
|
3073
|
+
else {
|
|
3074
|
+
if self.textStorage.string != self.lastAuthorizedText {
|
|
3075
|
+
self.scheduleReconciliationFromRust()
|
|
3076
|
+
}
|
|
3077
|
+
return
|
|
3078
|
+
}
|
|
3079
|
+
guard self.textStorage.string == mutation.resultingText else {
|
|
3080
|
+
if self.textStorage.string != self.lastAuthorizedText {
|
|
3081
|
+
self.scheduleReconciliationFromRust()
|
|
3082
|
+
}
|
|
3083
|
+
return
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
self.performInterceptedInput {
|
|
3087
|
+
if mutation.from == mutation.to {
|
|
3088
|
+
guard !mutation.replacementText.isEmpty else { return }
|
|
3089
|
+
self.insertTextInRust(mutation.replacementText, at: mutation.from)
|
|
3090
|
+
} else if mutation.replacementText.isEmpty {
|
|
3091
|
+
self.deleteScalarRangeInRust(from: mutation.from, to: mutation.to)
|
|
3092
|
+
} else {
|
|
3093
|
+
self.replaceTextRangeInRust(
|
|
3094
|
+
from: mutation.from,
|
|
3095
|
+
to: mutation.to,
|
|
3096
|
+
with: mutation.replacementText
|
|
3097
|
+
)
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3000
3103
|
private func insertNodeInRust(_ nodeType: String) {
|
|
3001
3104
|
guard let selection = currentScalarSelection() else { return }
|
|
3002
3105
|
Self.inputLog.debug(
|
|
@@ -4443,6 +4546,13 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4443
4546
|
let currentText = textStorage.string
|
|
4444
4547
|
guard currentText != lastAuthorizedText else { return }
|
|
4445
4548
|
currentTopLevelChildMetadata = nil
|
|
4549
|
+
|
|
4550
|
+
if shouldAdoptNativeTextStorageMutation(),
|
|
4551
|
+
let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText) {
|
|
4552
|
+
scheduleNativeTextMutationCommit(mutation)
|
|
4553
|
+
return
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4446
4556
|
let authorizedPreview = preview(lastAuthorizedText)
|
|
4447
4557
|
let storagePreview = preview(currentText)
|
|
4448
4558
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-prose-editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
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-prose-editor",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|