@apollohg/react-native-prose-editor 0.4.0 → 0.4.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 (30) hide show
  1. package/README.md +18 -0
  2. package/android/build.gradle +23 -0
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
  7. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
  9. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
  10. package/dist/NativeEditorBridge.d.ts +36 -1
  11. package/dist/NativeEditorBridge.js +173 -94
  12. package/dist/NativeRichTextEditor.d.ts +2 -0
  13. package/dist/NativeRichTextEditor.js +160 -53
  14. package/dist/YjsCollaboration.d.ts +2 -0
  15. package/dist/YjsCollaboration.js +142 -20
  16. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  17. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  18. package/ios/EditorLayoutManager.swift +3 -3
  19. package/ios/Generated_editor_core.swift +41 -0
  20. package/ios/NativeEditorExpoView.swift +43 -11
  21. package/ios/NativeEditorModule.swift +6 -0
  22. package/ios/PositionBridge.swift +310 -75
  23. package/ios/RenderBridge.swift +362 -27
  24. package/ios/RichTextEditorView.swift +1983 -187
  25. package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
  26. package/package.json +11 -2
  27. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  28. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  29. package/rust/android/x86_64/libeditor_core.so +0 -0
  30. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +63 -0
@@ -29,7 +29,9 @@ import org.json.JSONArray
29
29
  import org.json.JSONObject
30
30
  import java.lang.ref.WeakReference
31
31
  import java.net.URL
32
- import java.util.concurrent.Executors
32
+ import java.util.concurrent.LinkedBlockingQueue
33
+ import java.util.concurrent.ThreadPoolExecutor
34
+ import java.util.concurrent.TimeUnit
33
35
 
34
36
  object LayoutConstants {
35
37
  /** Base indentation per depth level (pixels at base scale). */
@@ -79,6 +81,7 @@ data class BlockContext(
79
81
  val nodeType: String,
80
82
  val depth: Int,
81
83
  val listContext: JSONObject?,
84
+ val topLevelChildIndex: Int? = null,
82
85
  var markerPending: Boolean = false,
83
86
  var renderStart: Int = 0
84
87
  )
@@ -218,10 +221,30 @@ class HorizontalRuleSpan(
218
221
  private val lineColor: Int,
219
222
  private val lineHeight: Float = LayoutConstants.HORIZONTAL_RULE_HEIGHT,
220
223
  private val verticalPadding: Float = LayoutConstants.HORIZONTAL_RULE_VERTICAL_PADDING
221
- ) : LeadingMarginSpan {
224
+ ) : ReplacementSpan(), LeadingMarginSpan {
222
225
 
223
226
  override fun getLeadingMargin(first: Boolean): Int = 0
224
227
 
228
+ override fun getSize(
229
+ paint: Paint,
230
+ text: CharSequence,
231
+ start: Int,
232
+ end: Int,
233
+ fm: Paint.FontMetricsInt?
234
+ ): Int {
235
+ if (fm != null) {
236
+ val totalHeight = kotlin.math.ceil(lineHeight + (verticalPadding * 2)).toInt()
237
+ val halfHeight = totalHeight / 2
238
+ fm.ascent = -halfHeight
239
+ fm.top = fm.ascent
240
+ fm.descent = totalHeight - halfHeight
241
+ fm.bottom = fm.descent
242
+ }
243
+ // Keep the placeholder atom in the text model without reserving
244
+ // visible glyph width, so Android does not paint a tofu/OBJ box.
245
+ return 0
246
+ }
247
+
225
248
  override fun drawLeadingMargin(
226
249
  canvas: Canvas,
227
250
  paint: Paint,
@@ -255,6 +278,21 @@ class HorizontalRuleSpan(
255
278
  paint.color = savedColor
256
279
  paint.style = savedStyle
257
280
  }
281
+
282
+ override fun draw(
283
+ canvas: Canvas,
284
+ text: CharSequence,
285
+ start: Int,
286
+ end: Int,
287
+ x: Float,
288
+ top: Int,
289
+ y: Int,
290
+ bottom: Int,
291
+ paint: Paint
292
+ ) {
293
+ // Intentionally empty: drawLeadingMargin renders the separator line,
294
+ // and ReplacementSpan suppresses drawing the underlying FFFC glyph.
295
+ }
258
296
  }
259
297
 
260
298
  internal object RenderImageDecoder {
@@ -365,14 +403,36 @@ internal object RenderImageDecoder {
365
403
  }
366
404
  }
367
405
 
368
- private object RenderImageLoader {
406
+ internal object RenderImageLoader {
369
407
  private val cache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
370
408
  override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
371
409
  }
372
- private val executor = Executors.newCachedThreadPool()
410
+ private val executor =
411
+ ThreadPoolExecutor(
412
+ 2,
413
+ 2,
414
+ 30L,
415
+ TimeUnit.SECONDS,
416
+ LinkedBlockingQueue()
417
+ )
418
+ private val lock = Any()
419
+ private val inFlight = mutableMapOf<String, MutableList<(Bitmap?) -> Unit>>()
420
+
421
+ @Volatile
422
+ internal var decodeSourceOverride: ((String) -> Bitmap?)? = null
373
423
 
374
424
  fun cached(source: String): Bitmap? = synchronized(cache) { cache.get(source) }
375
425
 
426
+ internal fun resetForTesting() {
427
+ synchronized(cache) {
428
+ cache.evictAll()
429
+ }
430
+ synchronized(lock) {
431
+ inFlight.clear()
432
+ }
433
+ decodeSourceOverride = null
434
+ }
435
+
376
436
  fun load(source: String, onLoaded: (Bitmap?) -> Unit) {
377
437
  cached(source)?.let {
378
438
  onLoaded(it)
@@ -380,7 +440,7 @@ private object RenderImageLoader {
380
440
  }
381
441
 
382
442
  if (source.trim().startsWith("data:image/", ignoreCase = true)) {
383
- val bitmap = RenderImageDecoder.decodeSource(source)
443
+ val bitmap = decode(source)
384
444
  if (bitmap != null) {
385
445
  synchronized(cache) {
386
446
  cache.put(source, bitmap)
@@ -390,16 +450,32 @@ private object RenderImageLoader {
390
450
  return
391
451
  }
392
452
 
453
+ synchronized(lock) {
454
+ val existing = inFlight[source]
455
+ if (existing != null) {
456
+ existing += onLoaded
457
+ return
458
+ }
459
+ inFlight[source] = mutableListOf(onLoaded)
460
+ }
461
+
393
462
  executor.execute {
394
- val bitmap = RenderImageDecoder.decodeSource(source)
463
+ val bitmap = decode(source)
395
464
  if (bitmap != null) {
396
465
  synchronized(cache) {
397
466
  cache.put(source, bitmap)
398
467
  }
399
468
  }
400
- onLoaded(bitmap)
469
+ val callbacks = synchronized(lock) {
470
+ inFlight.remove(source) ?: mutableListOf()
471
+ }
472
+ callbacks.forEach { it(bitmap) }
401
473
  }
402
474
  }
475
+
476
+ private fun decode(source: String): Bitmap? {
477
+ return decodeSourceOverride?.invoke(source) ?: RenderImageDecoder.decodeSource(source)
478
+ }
403
479
  }
404
480
 
405
481
  internal class BlockImageSpan(
@@ -675,8 +751,17 @@ class CenteredBulletSpan(
675
751
 
676
752
  object RenderBridge {
677
753
  internal const val NATIVE_BLOCKQUOTE_ANNOTATION = "nativeBlockquote"
754
+ internal const val NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION = "nativeTopLevelChildIndex"
678
755
  private const val NATIVE_SYNTHETIC_PLACEHOLDER_ANNOTATION = "nativeSyntheticPlaceholder"
679
756
 
757
+ private data class RenderBuildState(
758
+ val result: SpannableStringBuilder = SpannableStringBuilder(),
759
+ val blockStack: MutableList<BlockContext> = mutableListOf(),
760
+ val pendingLeadingMargins: MutableMap<Int, PendingLeadingMargin> = linkedMapOf(),
761
+ var isFirstBlock: Boolean = true,
762
+ var nextBlockSpacingBefore: Float? = null
763
+ )
764
+
680
765
  fun buildSpannable(
681
766
  json: String,
682
767
  baseFontSize: Float,
@@ -702,12 +787,57 @@ object RenderBridge {
702
787
  density: Float = 1f,
703
788
  hostView: TextView? = null
704
789
  ): SpannableStringBuilder {
705
- val result = SpannableStringBuilder()
706
- val blockStack = mutableListOf<BlockContext>()
707
- val pendingLeadingMargins = linkedMapOf<Int, PendingLeadingMargin>()
708
- var isFirstBlock = true
709
- var nextBlockSpacingBefore: Float? = null
790
+ val state = RenderBuildState()
791
+ appendElements(
792
+ state = state,
793
+ elements = elements,
794
+ baseFontSize = baseFontSize,
795
+ textColor = textColor,
796
+ theme = theme,
797
+ density = density,
798
+ hostView = hostView
799
+ )
800
+ applyPendingLeadingMargins(state.result, state.pendingLeadingMargins)
801
+ return state.result
802
+ }
710
803
 
804
+ fun buildSpannableFromBlocks(
805
+ blocks: JSONArray,
806
+ startIndex: Int = 0,
807
+ baseFontSize: Float,
808
+ textColor: Int,
809
+ theme: EditorTheme? = null,
810
+ density: Float = 1f,
811
+ hostView: TextView? = null
812
+ ): SpannableStringBuilder {
813
+ val state = RenderBuildState()
814
+ for (blockOffset in 0 until blocks.length()) {
815
+ val blockElements = blocks.optJSONArray(blockOffset) ?: continue
816
+ appendElements(
817
+ state = state,
818
+ elements = blockElements,
819
+ baseFontSize = baseFontSize,
820
+ textColor = textColor,
821
+ theme = theme,
822
+ density = density,
823
+ hostView = hostView,
824
+ topLevelChildIndex = startIndex + blockOffset
825
+ )
826
+ }
827
+ applyPendingLeadingMargins(state.result, state.pendingLeadingMargins)
828
+ return state.result
829
+ }
830
+
831
+ private fun appendElements(
832
+ state: RenderBuildState,
833
+ elements: JSONArray,
834
+ baseFontSize: Float,
835
+ textColor: Int,
836
+ theme: EditorTheme?,
837
+ density: Float,
838
+ hostView: TextView?,
839
+ topLevelChildIndex: Int? = null
840
+ ) {
711
841
  for (i in 0 until elements.length()) {
712
842
  val element = elements.optJSONObject(i) ?: continue
713
843
  val type = element.optString("type", "")
@@ -718,13 +848,13 @@ object RenderBridge {
718
848
  val marksArray = element.optJSONArray("marks")
719
849
  val marks = parseMarks(marksArray)
720
850
  appendStyledText(
721
- result,
851
+ state.result,
722
852
  text,
723
853
  marks,
724
854
  baseFontSize,
725
855
  textColor,
726
- blockStack,
727
- pendingLeadingMargins,
856
+ state.blockStack,
857
+ state.pendingLeadingMargins,
728
858
  theme,
729
859
  density
730
860
  )
@@ -733,12 +863,12 @@ object RenderBridge {
733
863
  "voidInline" -> {
734
864
  val nodeType = element.optString("nodeType", "")
735
865
  appendVoidInline(
736
- result,
866
+ state.result,
737
867
  nodeType,
738
868
  baseFontSize,
739
869
  textColor,
740
- blockStack,
741
- pendingLeadingMargins,
870
+ state.blockStack,
871
+ state.pendingLeadingMargins,
742
872
  theme,
743
873
  density
744
874
  )
@@ -747,16 +877,22 @@ object RenderBridge {
747
877
  "voidBlock" -> {
748
878
  val nodeType = element.optString("nodeType", "")
749
879
  val attrs = element.optJSONObject("attrs")
750
- if (!isFirstBlock) {
751
- val spacingPx = ((nextBlockSpacingBefore ?: 0f) * density).toInt()
752
- appendInterBlockNewline(result, baseFontSize, textColor, spacingPx)
880
+ if (!state.isFirstBlock) {
881
+ val spacingPx = ((state.nextBlockSpacingBefore ?: 0f) * density).toInt()
882
+ appendInterBlockNewline(
883
+ state.result,
884
+ baseFontSize,
885
+ textColor,
886
+ spacingPx,
887
+ topLevelChildIndex = topLevelChildIndex
888
+ )
753
889
  }
754
- isFirstBlock = false
890
+ state.isFirstBlock = false
755
891
  val spacingBefore = theme?.effectiveTextStyle(nodeType)?.spacingAfter
756
892
  ?: theme?.list?.itemSpacing
757
- nextBlockSpacingBefore = spacingBefore
893
+ state.nextBlockSpacingBefore = spacingBefore
758
894
  appendVoidBlock(
759
- result,
895
+ state.result,
760
896
  nodeType,
761
897
  attrs,
762
898
  baseFontSize,
@@ -764,7 +900,8 @@ object RenderBridge {
764
900
  theme,
765
901
  density,
766
902
  spacingBefore,
767
- hostView
903
+ hostView,
904
+ topLevelChildIndex
768
905
  )
769
906
  }
770
907
 
@@ -772,13 +909,13 @@ object RenderBridge {
772
909
  val nodeType = element.optString("nodeType", "")
773
910
  val label = element.optString("label", "?")
774
911
  appendOpaqueInlineAtom(
775
- result,
912
+ state.result,
776
913
  nodeType,
777
914
  label,
778
915
  baseFontSize,
779
916
  textColor,
780
- blockStack,
781
- pendingLeadingMargins,
917
+ state.blockStack,
918
+ state.pendingLeadingMargins,
782
919
  theme,
783
920
  density
784
921
  )
@@ -788,13 +925,28 @@ object RenderBridge {
788
925
  val nodeType = element.optString("nodeType", "")
789
926
  val label = element.optString("label", "?")
790
927
  val blockSpacing = theme?.effectiveTextStyle(nodeType)?.spacingAfter
791
- if (!isFirstBlock) {
792
- val spacingPx = ((nextBlockSpacingBefore ?: 0f) * density).toInt()
793
- appendInterBlockNewline(result, baseFontSize, textColor, spacingPx)
928
+ if (!state.isFirstBlock) {
929
+ val spacingPx = ((state.nextBlockSpacingBefore ?: 0f) * density).toInt()
930
+ appendInterBlockNewline(
931
+ state.result,
932
+ baseFontSize,
933
+ textColor,
934
+ spacingPx,
935
+ topLevelChildIndex = topLevelChildIndex
936
+ )
794
937
  }
795
- isFirstBlock = false
796
- nextBlockSpacingBefore = blockSpacing
797
- appendOpaqueBlockAtom(result, nodeType, label, baseFontSize, textColor, theme, blockSpacing)
938
+ state.isFirstBlock = false
939
+ state.nextBlockSpacingBefore = blockSpacing
940
+ appendOpaqueBlockAtom(
941
+ state.result,
942
+ nodeType,
943
+ label,
944
+ baseFontSize,
945
+ textColor,
946
+ theme,
947
+ blockSpacing,
948
+ topLevelChildIndex
949
+ )
798
950
  }
799
951
 
800
952
  "blockStart" -> {
@@ -804,7 +956,7 @@ object RenderBridge {
804
956
  val isListItemContainer = nodeType == "listItem" && listContext != null
805
957
  val isTransparentContainer = nodeType == "blockquote"
806
958
  val nestedListItemContainer =
807
- isListItemContainer && blockStack.any { it.nodeType == "listItem" && it.listContext != null }
959
+ isListItemContainer && state.blockStack.any { it.nodeType == "listItem" && it.listContext != null }
808
960
  val blockSpacing = if (isListItemContainer) {
809
961
  null
810
962
  } else {
@@ -813,44 +965,47 @@ object RenderBridge {
813
965
  }
814
966
 
815
967
  if (!isListItemContainer && !isTransparentContainer) {
816
- if (!isFirstBlock) {
817
- val spacingPx = ((nextBlockSpacingBefore ?: 0f) * density).toInt()
818
- val nextBlockStack = blockStack + BlockContext(
968
+ if (!state.isFirstBlock) {
969
+ val spacingPx = ((state.nextBlockSpacingBefore ?: 0f) * density).toInt()
970
+ val nextBlockStack = state.blockStack + BlockContext(
819
971
  nodeType = nodeType,
820
972
  depth = depth,
821
973
  listContext = listContext,
974
+ topLevelChildIndex = topLevelChildIndex,
822
975
  markerPending = isListItemContainer,
823
- renderStart = result.length
976
+ renderStart = state.result.length
824
977
  )
825
978
  val inBlockquoteSeparator =
826
- blockquoteDepth(nextBlockStack) > 0f && trailingRenderedContentHasBlockquote(result)
979
+ blockquoteDepth(nextBlockStack) > 0f && trailingRenderedContentHasBlockquote(state.result)
827
980
  appendInterBlockNewline(
828
- result,
981
+ state.result,
829
982
  baseFontSize,
830
983
  textColor,
831
984
  spacingPx,
832
- inBlockquote = inBlockquoteSeparator
985
+ inBlockquote = inBlockquoteSeparator,
986
+ topLevelChildIndex = topLevelChildIndex
833
987
  )
834
988
  }
835
- isFirstBlock = false
836
- nextBlockSpacingBefore = blockSpacing
989
+ state.isFirstBlock = false
990
+ state.nextBlockSpacingBefore = blockSpacing
837
991
  } else if (nestedListItemContainer && theme?.list?.itemSpacing != null) {
838
- nextBlockSpacingBefore = theme.list.itemSpacing
992
+ state.nextBlockSpacingBefore = theme.list.itemSpacing
839
993
  }
840
994
 
841
995
  val ctx = BlockContext(
842
996
  nodeType = nodeType,
843
997
  depth = depth,
844
998
  listContext = listContext,
999
+ topLevelChildIndex = topLevelChildIndex,
845
1000
  markerPending = isListItemContainer,
846
- renderStart = result.length
1001
+ renderStart = state.result.length
847
1002
  )
848
- blockStack.add(ctx)
1003
+ state.blockStack.add(ctx)
849
1004
 
850
1005
  val markerListContext = when {
851
1006
  isListItemContainer -> null
852
1007
  listContext != null -> listContext
853
- else -> consumePendingListMarker(blockStack, result.length)
1008
+ else -> consumePendingListMarker(state.blockStack, state.result.length)
854
1009
  }
855
1010
 
856
1011
  if (markerListContext != null) {
@@ -860,33 +1015,34 @@ object RenderBridge {
860
1015
  resolveTextStyle(
861
1016
  nodeType,
862
1017
  theme,
863
- blockquoteDepth(blockStack) > 0
1018
+ blockquoteDepth(state.blockStack) > 0
864
1019
  ).fontSize?.times(density) ?: baseFontSize
865
1020
  val markerTextStyle = resolveTextStyle(
866
1021
  nodeType,
867
1022
  theme,
868
- blockquoteDepth(blockStack) > 0
1023
+ blockquoteDepth(state.blockStack) > 0
869
1024
  )
870
1025
  appendStyledText(
871
- result,
1026
+ state.result,
872
1027
  marker,
873
1028
  emptyList(),
874
1029
  markerBaseSize,
875
1030
  theme?.list?.markerColor ?: textColor,
876
- blockStack,
877
- pendingLeadingMargins,
1031
+ state.blockStack,
1032
+ state.pendingLeadingMargins,
878
1033
  null,
879
1034
  density,
880
1035
  applyBlockSpans = false
881
1036
  )
1037
+ val markerStart = state.result.length - marker.length
1038
+ val markerEnd = state.result.length
1039
+ annotateTopLevelChild(state.result, markerStart, markerEnd, topLevelChildIndex)
882
1040
  if (!ordered) {
883
- val markerStart = result.length - marker.length
884
- val markerEnd = result.length
885
1041
  val markerScale =
886
1042
  theme?.list?.markerScale ?: LayoutConstants.UNORDERED_LIST_MARKER_FONT_SCALE
887
1043
  val markerWidth = calculateMarkerWidth(density)
888
1044
  val bulletRadius = ((markerBaseSize * markerScale) * 0.16f).coerceAtLeast(2f * density)
889
- result.setSpan(
1045
+ state.result.setSpan(
890
1046
  CenteredBulletSpan(
891
1047
  textColor = theme?.list?.markerColor ?: textColor,
892
1048
  markerWidthPx = markerWidth,
@@ -900,9 +1056,9 @@ object RenderBridge {
900
1056
  )
901
1057
  }
902
1058
  applyLineHeightSpan(
903
- builder = result,
904
- start = result.length - marker.length,
905
- end = result.length,
1059
+ builder = state.result,
1060
+ start = markerStart,
1061
+ end = markerEnd,
906
1062
  lineHeight = markerTextStyle.lineHeight,
907
1063
  density = density
908
1064
  )
@@ -910,28 +1066,25 @@ object RenderBridge {
910
1066
  }
911
1067
 
912
1068
  "blockEnd" -> {
913
- if (blockStack.isNotEmpty()) {
914
- val endedBlock = blockStack.removeAt(blockStack.lastIndex)
1069
+ if (state.blockStack.isNotEmpty()) {
1070
+ val endedBlock = state.blockStack.removeAt(state.blockStack.lastIndex)
915
1071
  appendTrailingHardBreakPlaceholderIfNeeded(
916
- builder = result,
1072
+ builder = state.result,
917
1073
  endedBlock = endedBlock,
918
- remainingBlockStack = blockStack,
1074
+ remainingBlockStack = state.blockStack,
919
1075
  baseFontSize = baseFontSize,
920
1076
  textColor = textColor,
921
1077
  theme = theme,
922
1078
  density = density,
923
- pendingLeadingMargins = pendingLeadingMargins
1079
+ pendingLeadingMargins = state.pendingLeadingMargins
924
1080
  )
925
1081
  if (endedBlock.nodeType == "listItem" && endedBlock.listContext != null) {
926
- nextBlockSpacingBefore = theme?.list?.itemSpacing
1082
+ state.nextBlockSpacingBefore = theme?.list?.itemSpacing
927
1083
  }
928
1084
  }
929
1085
  }
930
1086
  }
931
1087
  }
932
-
933
- applyPendingLeadingMargins(result, pendingLeadingMargins)
934
- return result
935
1088
  }
936
1089
 
937
1090
  /**
@@ -1126,7 +1279,8 @@ object RenderBridge {
1126
1279
  theme: EditorTheme?,
1127
1280
  density: Float,
1128
1281
  spacingBefore: Float?,
1129
- hostView: TextView?
1282
+ hostView: TextView?,
1283
+ topLevelChildIndex: Int?
1130
1284
  ) {
1131
1285
  when (nodeType) {
1132
1286
  "horizontalRule" -> {
@@ -1148,6 +1302,7 @@ object RenderBridge {
1148
1302
  ),
1149
1303
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1150
1304
  )
1305
+ annotateTopLevelChild(builder, start, end, topLevelChildIndex)
1151
1306
  }
1152
1307
  "image" -> {
1153
1308
  val source = if (attrs != null && attrs.has("src") && !attrs.isNull("src")) {
@@ -1174,9 +1329,12 @@ object RenderBridge {
1174
1329
  ),
1175
1330
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1176
1331
  )
1332
+ annotateTopLevelChild(builder, start, end, topLevelChildIndex)
1177
1333
  }
1178
1334
  else -> {
1335
+ val start = builder.length
1179
1336
  builder.append(LayoutConstants.OBJECT_REPLACEMENT_CHARACTER)
1337
+ annotateTopLevelChild(builder, start, builder.length, topLevelChildIndex)
1180
1338
  }
1181
1339
  }
1182
1340
  }
@@ -1238,7 +1396,8 @@ object RenderBridge {
1238
1396
  baseFontSize: Float,
1239
1397
  textColor: Int,
1240
1398
  theme: EditorTheme?,
1241
- spacingBefore: Float?
1399
+ spacingBefore: Float?,
1400
+ topLevelChildIndex: Int?
1242
1401
  ) {
1243
1402
  val text = if (nodeType == "mention") label else "[$label]"
1244
1403
  val start = builder.length
@@ -1256,6 +1415,7 @@ object RenderBridge {
1256
1415
  Annotation("nativeVoidNodeType", nodeType),
1257
1416
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1258
1417
  )
1418
+ annotateTopLevelChild(builder, start, end, topLevelChildIndex)
1259
1419
  }
1260
1420
 
1261
1421
  private fun applyBlockStyle(
@@ -1334,6 +1494,7 @@ object RenderBridge {
1334
1494
  Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1335
1495
  )
1336
1496
  }
1497
+ annotateTopLevelChild(builder, start, end, currentBlock.topLevelChildIndex)
1337
1498
 
1338
1499
  val lineHeight = resolveTextStyle(
1339
1500
  currentBlock.nodeType,
@@ -1599,7 +1760,8 @@ object RenderBridge {
1599
1760
  baseFontSize: Float,
1600
1761
  textColor: Int,
1601
1762
  spacingPx: Int = 0,
1602
- inBlockquote: Boolean = false
1763
+ inBlockquote: Boolean = false,
1764
+ topLevelChildIndex: Int? = null
1603
1765
  ) {
1604
1766
  val start = builder.length
1605
1767
  builder.append("\n")
@@ -1619,6 +1781,7 @@ object RenderBridge {
1619
1781
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1620
1782
  )
1621
1783
  }
1784
+ annotateTopLevelChild(builder, start, end, topLevelChildIndex)
1622
1785
  if (inBlockquote) {
1623
1786
  builder.setSpan(
1624
1787
  Annotation(NATIVE_BLOCKQUOTE_ANNOTATION, "1"),
@@ -1677,6 +1840,21 @@ object RenderBridge {
1677
1840
  }
1678
1841
  }
1679
1842
 
1843
+ private fun annotateTopLevelChild(
1844
+ builder: SpannableStringBuilder,
1845
+ start: Int,
1846
+ end: Int,
1847
+ topLevelChildIndex: Int?
1848
+ ) {
1849
+ if (topLevelChildIndex == null || start >= end) return
1850
+ builder.setSpan(
1851
+ Annotation(NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION, topLevelChildIndex.toString()),
1852
+ start,
1853
+ end,
1854
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1855
+ )
1856
+ }
1857
+
1680
1858
  private fun trailingRenderedContentHasBlockquote(builder: Spanned): Boolean {
1681
1859
  for (index in builder.length - 1 downTo 0) {
1682
1860
  val ch = builder[index]
@@ -182,7 +182,8 @@ class RichTextEditorView @JvmOverloads constructor(
182
182
  }
183
183
 
184
184
  fun refreshRemoteSelections() {
185
- remoteSelectionOverlayView.invalidate()
185
+ if (!remoteSelectionOverlayView.hasSelectionsOrCachedGeometry()) return
186
+ remoteSelectionOverlayView.refreshGeometry()
186
187
  }
187
188
 
188
189
  fun imageResizeOverlayRectForTesting(): android.graphics.RectF? =
@@ -205,14 +206,14 @@ class RichTextEditorView @JvmOverloads constructor(
205
206
 
206
207
  fun setContent(html: String) {
207
208
  if (editorId == 0L) return
208
- val renderJSON = editorSetHtml(editorId.toULong(), html)
209
- editorEditText.applyRenderJSON(renderJSON)
209
+ editorSetHtml(editorId.toULong(), html)
210
+ editorEditText.applyUpdateJSON(editorGetCurrentState(editorId.toULong()), notifyListener = false)
210
211
  }
211
212
 
212
213
  fun setContent(json: org.json.JSONObject) {
213
214
  if (editorId == 0L) return
214
- val renderJSON = editorSetJson(editorId.toULong(), json.toString())
215
- editorEditText.applyRenderJSON(renderJSON)
215
+ editorSetJson(editorId.toULong(), json.toString())
216
+ editorEditText.applyUpdateJSON(editorGetCurrentState(editorId.toULong()), notifyListener = false)
216
217
  }
217
218
 
218
219
  override fun onDetachedFromWindow() {
@@ -322,7 +323,7 @@ class RichTextEditorView @JvmOverloads constructor(
322
323
  }
323
324
 
324
325
  private fun refreshOverlays() {
325
- remoteSelectionOverlayView.invalidate()
326
+ remoteSelectionOverlayView.refreshGeometry()
326
327
  imageResizeOverlayView.refresh()
327
328
  }
328
329
  }