@apollohg/react-native-prose-editor 0.5.1 → 0.5.3

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 (31) hide show
  1. package/README.md +18 -15
  2. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +33 -1
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +39 -6
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +15 -1
  7. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +44 -7
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +24 -4
  9. package/dist/NativeEditorBridge.d.ts +8 -0
  10. package/dist/NativeEditorBridge.js +16 -0
  11. package/dist/NativeProseViewer.d.ts +25 -5
  12. package/dist/NativeProseViewer.js +212 -13
  13. package/dist/NativeRichTextEditor.d.ts +2 -0
  14. package/dist/NativeRichTextEditor.js +417 -31
  15. package/dist/addons.d.ts +20 -0
  16. package/dist/addons.js +4 -0
  17. package/dist/index.d.ts +2 -2
  18. package/ios/EditorAddons.swift +2 -0
  19. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  20. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  21. package/ios/EditorLayoutManager.swift +10 -1
  22. package/ios/EditorTheme.swift +25 -0
  23. package/ios/NativeEditorExpoView.swift +56 -6
  24. package/ios/NativeEditorModule.swift +14 -1
  25. package/ios/NativeProseViewerExpoView.swift +62 -11
  26. package/ios/RenderBridge.swift +40 -16
  27. package/ios/RichTextEditorView.swift +4 -0
  28. package/package.json +1 -1
  29. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  30. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  31. package/rust/android/x86_64/libeditor_core.so +0 -0
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  This project is currently in `alpha` and the API, behavior, and packaging may still change.
6
6
 
7
7
  <p align="center">
8
- <img src="./docs/images/example-ios.png" alt="Example editor Android" width="45%" align="top" />
9
- <img src="./docs/images/example-android.png" alt="Example editor iOS" width="45%" align="top" />
8
+ <img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-ios.png" alt="Example editor iOS" width="45%" align="top" />
9
+ <img src="https://raw.githubusercontent.com/wiki/apollohg/react-native-prose-editor/images/example-android.png" alt="Example editor Android" width="45%" align="top" />
10
10
  </p>
11
11
 
12
12
  This repository contains three main pieces:
@@ -38,7 +38,8 @@ The editor already supports:
38
38
  - [`android`](./android): Android native view, rendering bridge, and Expo module wiring
39
39
  - [`Rust Editor Core`](./rust/editor-core): document model, transforms, schema system, selection, history, serialization, and tests
40
40
  - [`example`](./example): Expo 54 app for manual QA and development
41
- - [`docs`](./docs): project documentation
41
+
42
+ Project documentation now lives in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
42
43
 
43
44
  ## Installation
44
45
 
@@ -67,7 +68,7 @@ npm --prefix example install
67
68
  npm run example:prebuild
68
69
  ```
69
70
 
70
- For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](./docs/guides/installation.md).
71
+ For full setup details, including peer dependencies, example app setup, and iOS pods, see the [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation).
71
72
 
72
73
  ## Basic Usage
73
74
 
@@ -106,9 +107,9 @@ The main extension points today are:
106
107
  - `addons`: configure optional features like @-mentions
107
108
  - `heightBehavior`: switch between internal scrolling and auto-grow
108
109
 
109
- For setup and customization details, start with the [Documentation Index](./docs/README.md).
110
+ For setup and customization details, start with the [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki).
110
111
 
111
- For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](./docs/modules/collaboration.md).
112
+ For realtime collaboration, including the correct `useYjsCollaboration()` wiring, encoded-state persistence, remote cursors, and automatic reconnect behavior, see the [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration).
112
113
 
113
114
  For whole-document JSON loads, `initialJSON`, controlled `valueJSON`, and `setContentJson()` will normalize an empty root document like `{ type: 'doc', content: [] }` to the active schema's empty text block so block-constrained schemas still load a valid empty document.
114
115
 
@@ -152,15 +153,17 @@ npm run ios:test:perf:device
152
153
 
153
154
  ## Documentation
154
155
 
155
- - [Documentation Index](./docs/README.md): main documentation index
156
- - [Installation Guide](./docs/guides/installation.md): installation and local setup
157
- - [Getting Started](./docs/guides/getting-started.md): first setup and first editor
158
- - [Collaboration Guide](./docs/modules/collaboration.md): Yjs collaboration wiring, source-of-truth rules, and persistence
159
- - [Toolbar Setup](./docs/guides/toolbar-setup.md): toolbar setup patterns and examples
160
- - [Mentions Guide](./docs/modules/mentions.md): @-mentions addon setup and configuration
161
- - [Styling Guide](./docs/guides/styling.md): content, toolbar, and mention styling
162
- - [NativeRichTextEditor Reference](./docs/reference/native-rich-text-editor.md): component props and ref methods
163
- - [Design Decisions](./docs/explanations/design-decisions.md): rationale for key API and architecture decisions
156
+ Documentation is published in the [GitHub Wiki](https://github.com/apollohg/react-native-prose-editor/wiki).
157
+
158
+ - [Documentation Index](https://github.com/apollohg/react-native-prose-editor/wiki): main documentation index
159
+ - [Installation Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Installation): installation and local setup
160
+ - [Getting Started](https://github.com/apollohg/react-native-prose-editor/wiki/Getting-Started): first setup and first editor
161
+ - [Collaboration Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Collaboration): Yjs collaboration wiring, source-of-truth rules, and persistence
162
+ - [Toolbar Setup](https://github.com/apollohg/react-native-prose-editor/wiki/Toolbar-Setup): toolbar setup patterns and examples
163
+ - [Mentions Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Mentions): @-mentions addon setup and configuration
164
+ - [Styling Guide](https://github.com/apollohg/react-native-prose-editor/wiki/Styling): content, toolbar, and mention styling
165
+ - [NativeRichTextEditor Reference](https://github.com/apollohg/react-native-prose-editor/wiki/NativeRichTextEditor-Reference): component props and ref methods
166
+ - [Design Decisions](https://github.com/apollohg/react-native-prose-editor/wiki/Design-Decisions): rationale for key API and architecture decisions
164
167
 
165
168
  ## Project Status
166
169
 
@@ -30,7 +30,8 @@ data class NativeMentionSuggestion(
30
30
  data class NativeMentionsAddonConfig(
31
31
  val trigger: String,
32
32
  val suggestions: List<NativeMentionSuggestion>,
33
- val theme: EditorMentionTheme?
33
+ val theme: EditorMentionTheme?,
34
+ val resolveSelectionAttrs: Boolean
34
35
  ) {
35
36
  companion object {
36
37
  fun fromJson(json: JSONObject?): NativeMentionsAddonConfig? {
@@ -51,7 +52,8 @@ data class NativeMentionsAddonConfig(
51
52
  return NativeMentionsAddonConfig(
52
53
  trigger = trigger,
53
54
  suggestions = suggestions,
54
- theme = EditorMentionTheme.fromJson(json.optJSONObject("theme"))
55
+ theme = EditorMentionTheme.fromJson(json.optJSONObject("theme")),
56
+ resolveSelectionAttrs = json.optBoolean("resolveSelectionAttrs", false)
55
57
  )
56
58
  }
57
59
  }
@@ -77,6 +77,11 @@ class EditorEditText @JvmOverloads constructor(
77
77
  val label: String
78
78
  )
79
79
 
80
+ data class LinkHit(
81
+ val href: String,
82
+ val text: String
83
+ )
84
+
80
85
  private data class ParsedRenderPatch(
81
86
  val startIndex: Int,
82
87
  val deleteCount: Int,
@@ -1583,7 +1588,7 @@ class EditorEditText @JvmOverloads constructor(
1583
1588
  }
1584
1589
  }
1585
1590
 
1586
- fun mentionHitAt(x: Float, y: Float): MentionHit? {
1591
+ private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
1587
1592
  val spannable = text as? Spanned ?: return null
1588
1593
  val layout = layout ?: return null
1589
1594
  if (spannable.isEmpty()) return null
@@ -1603,6 +1608,11 @@ class EditorEditText @JvmOverloads constructor(
1603
1608
 
1604
1609
  val offset = layout.getOffsetForHorizontal(line, localX)
1605
1610
  .coerceIn(0, maxOf(spannable.length - 1, 0))
1611
+ return spannable to offset
1612
+ }
1613
+
1614
+ fun mentionHitAt(x: Float, y: Float): MentionHit? {
1615
+ val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
1606
1616
  val annotations = spannable.getSpans(
1607
1617
  offset,
1608
1618
  (offset + 1).coerceAtMost(spannable.length),
@@ -1626,6 +1636,28 @@ class EditorEditText @JvmOverloads constructor(
1626
1636
  )
1627
1637
  }
1628
1638
 
1639
+ fun linkHitAt(x: Float, y: Float): LinkHit? {
1640
+ val (spannable, offset) = textOffsetHitAt(x, y) ?: return null
1641
+ val annotations = spannable.getSpans(
1642
+ offset,
1643
+ (offset + 1).coerceAtMost(spannable.length),
1644
+ Annotation::class.java
1645
+ )
1646
+ val linkAnnotation = annotations.firstOrNull {
1647
+ it.key == RenderBridge.NATIVE_LINK_HREF_ANNOTATION && it.value.isNotBlank()
1648
+ } ?: return null
1649
+ val start = spannable.getSpanStart(linkAnnotation)
1650
+ val end = spannable.getSpanEnd(linkAnnotation)
1651
+ if (start < 0 || end <= start) {
1652
+ return null
1653
+ }
1654
+
1655
+ return LinkHit(
1656
+ href = linkAnnotation.value,
1657
+ text = spannable.subSequence(start, end).toString()
1658
+ )
1659
+ }
1660
+
1629
1661
  private fun handleImageTap(event: MotionEvent): Boolean {
1630
1662
  if (!imageResizingEnabled) {
1631
1663
  return false
@@ -127,6 +127,29 @@ data class EditorMentionTheme(
127
127
  val optionHighlightedBackgroundColor: Int? = null,
128
128
  val optionHighlightedTextColor: Int? = null
129
129
  ) {
130
+ fun mergedWith(other: EditorMentionTheme?): EditorMentionTheme {
131
+ other ?: return this
132
+ return copy(
133
+ textColor = other.textColor ?: textColor,
134
+ backgroundColor = other.backgroundColor ?: backgroundColor,
135
+ borderColor = other.borderColor ?: borderColor,
136
+ borderWidth = other.borderWidth ?: borderWidth,
137
+ borderRadius = other.borderRadius ?: borderRadius,
138
+ fontWeight = other.fontWeight ?: fontWeight,
139
+ popoverBackgroundColor = other.popoverBackgroundColor ?: popoverBackgroundColor,
140
+ popoverBorderColor = other.popoverBorderColor ?: popoverBorderColor,
141
+ popoverBorderWidth = other.popoverBorderWidth ?: popoverBorderWidth,
142
+ popoverBorderRadius = other.popoverBorderRadius ?: popoverBorderRadius,
143
+ popoverShadowColor = other.popoverShadowColor ?: popoverShadowColor,
144
+ optionTextColor = other.optionTextColor ?: optionTextColor,
145
+ optionSecondaryTextColor = other.optionSecondaryTextColor ?: optionSecondaryTextColor,
146
+ optionHighlightedBackgroundColor =
147
+ other.optionHighlightedBackgroundColor ?: optionHighlightedBackgroundColor,
148
+ optionHighlightedTextColor =
149
+ other.optionHighlightedTextColor ?: optionHighlightedTextColor
150
+ )
151
+ }
152
+
130
153
  companion object {
131
154
  fun fromJson(json: JSONObject?): EditorMentionTheme? {
132
155
  json ?: return null
@@ -529,12 +529,42 @@ class NativeEditorExpoView(
529
529
  onAddonEvent(mapOf("eventJson" to eventJson))
530
530
  }
531
531
 
532
- private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion) {
532
+ private fun resolvedMentionAttrs(
533
+ trigger: String,
534
+ suggestion: NativeMentionSuggestion
535
+ ): JSONObject {
536
+ val attrs = JSONObject(suggestion.attrs.toString())
537
+ if (!attrs.has("label")) {
538
+ attrs.put("label", suggestion.label)
539
+ }
540
+ if (!attrs.has("mentionSuggestionChar")) {
541
+ attrs.put("mentionSuggestionChar", trigger)
542
+ }
543
+ return attrs
544
+ }
545
+
546
+ private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion, attrs: JSONObject) {
533
547
  val eventJson = JSONObject()
534
548
  .put("type", "mentionsSelect")
535
549
  .put("trigger", trigger)
536
550
  .put("suggestionKey", suggestion.key)
537
- .put("attrs", suggestion.attrs)
551
+ .put("attrs", attrs)
552
+ .toString()
553
+ onAddonEvent(mapOf("eventJson" to eventJson))
554
+ }
555
+
556
+ private fun emitMentionSelectRequest(
557
+ trigger: String,
558
+ suggestion: NativeMentionSuggestion,
559
+ attrs: JSONObject,
560
+ range: MentionQueryState
561
+ ) {
562
+ val eventJson = JSONObject()
563
+ .put("type", "mentionsSelectRequest")
564
+ .put("trigger", trigger)
565
+ .put("suggestionKey", suggestion.key)
566
+ .put("attrs", attrs)
567
+ .put("range", JSONObject().put("anchor", range.anchor).put("head", range.head))
538
568
  .toString()
539
569
  onAddonEvent(mapOf("eventJson" to eventJson))
540
570
  }
@@ -542,9 +572,12 @@ class NativeEditorExpoView(
542
572
  private fun insertMentionSuggestion(suggestion: NativeMentionSuggestion) {
543
573
  val mentions = addons.mentions ?: return
544
574
  val queryState = mentionQueryState ?: return
545
- val attrs = JSONObject(suggestion.attrs.toString())
546
- if (!attrs.has("label")) {
547
- attrs.put("label", suggestion.label)
575
+ val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
576
+ if (mentions.resolveSelectionAttrs) {
577
+ emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState)
578
+ lastMentionEventJson = null
579
+ clearMentionQueryState()
580
+ return
548
581
  }
549
582
  val docJson = JSONObject()
550
583
  .put("type", "doc")
@@ -564,7 +597,7 @@ class NativeEditorExpoView(
564
597
  docJson.toString()
565
598
  )
566
599
  richTextView.editorEditText.applyUpdateJSON(updateJson)
567
- emitMentionSelect(mentions.trigger, suggestion)
600
+ emitMentionSelect(mentions.trigger, suggestion, attrs)
568
601
  lastMentionEventJson = null
569
602
  clearMentionQueryState()
570
603
  }
@@ -295,6 +295,14 @@ class NativeEditorModule : Module() {
295
295
  editorDestroy(editorId)
296
296
  }
297
297
  }
298
+ Function("renderDocumentHtml") { configJson: String, html: String ->
299
+ val editorId = editorCreate(configJson)
300
+ try {
301
+ editorSetHtml(editorId, html)
302
+ } finally {
303
+ editorDestroy(editorId)
304
+ }
305
+ }
298
306
 
299
307
  View(NativeEditorExpoView::class) {
300
308
  Events(
@@ -370,7 +378,7 @@ class NativeEditorModule : Module() {
370
378
 
371
379
  View(NativeProseViewerExpoView::class) {
372
380
  Name("NativeProseViewer")
373
- Events("onContentHeightChange", "onPressMention")
381
+ Events("onContentHeightChange", "onPressLink", "onPressMention")
374
382
 
375
383
  Prop("renderJson") { view: NativeProseViewerExpoView, renderJson: String? ->
376
384
  view.setRenderJson(renderJson)
@@ -378,6 +386,12 @@ class NativeEditorModule : Module() {
378
386
  Prop("themeJson") { view: NativeProseViewerExpoView, themeJson: String? ->
379
387
  view.setThemeJson(themeJson)
380
388
  }
389
+ Prop("enableLinkTaps") { view: NativeProseViewerExpoView, enableLinkTaps: Boolean? ->
390
+ view.setEnableLinkTaps(enableLinkTaps)
391
+ }
392
+ Prop("interceptLinkTaps") { view: NativeProseViewerExpoView, interceptLinkTaps: Boolean? ->
393
+ view.setInterceptLinkTaps(interceptLinkTaps)
394
+ }
381
395
  }
382
396
  }
383
397
  }
@@ -1,7 +1,9 @@
1
1
  package com.apollohg.editor
2
2
 
3
+ import android.content.Intent
3
4
  import android.content.Context
4
5
  import android.graphics.Color
6
+ import android.net.Uri
5
7
  import android.view.MotionEvent
6
8
  import android.view.View
7
9
  import android.view.ViewGroup
@@ -17,11 +19,15 @@ class NativeProseViewerExpoView(
17
19
  private val proseView = EditorEditText(context)
18
20
  private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
19
21
  @Suppress("unused")
22
+ private val onPressLink by EventDispatcher<Map<String, Any>>()
23
+ @Suppress("unused")
20
24
  private val onPressMention by EventDispatcher<Map<String, Any>>()
21
25
 
22
26
  private var lastRenderJson: String? = null
23
27
  private var lastThemeJson: String? = null
24
28
  private var lastEmittedContentHeight = 0
29
+ private var enableLinkTaps = true
30
+ private var interceptLinkTaps = false
25
31
 
26
32
  init {
27
33
  proseView.setBaseStyle(
@@ -43,14 +49,27 @@ class NativeProseViewerExpoView(
43
49
  return@setOnTouchListener false
44
50
  }
45
51
 
46
- val mention = proseView.mentionHitAt(event.x, event.y) ?: return@setOnTouchListener false
47
- onPressMention(
48
- mapOf(
49
- "docPos" to mention.docPos,
50
- "label" to mention.label
52
+ proseView.mentionHitAt(event.x, event.y)?.let { mention ->
53
+ onPressMention(mapOf("docPos" to mention.docPos, "label" to mention.label))
54
+ return@setOnTouchListener true
55
+ }
56
+
57
+ if (!enableLinkTaps) {
58
+ return@setOnTouchListener false
59
+ }
60
+
61
+ val link = proseView.linkHitAt(event.x, event.y) ?: return@setOnTouchListener false
62
+ if (interceptLinkTaps) {
63
+ onPressLink(
64
+ mapOf(
65
+ "href" to link.href,
66
+ "text" to link.text
67
+ )
51
68
  )
52
- )
53
- true
69
+ return@setOnTouchListener true
70
+ }
71
+
72
+ return@setOnTouchListener openLink(link.href)
54
73
  }
55
74
 
56
75
  addView(
@@ -83,6 +102,14 @@ class NativeProseViewerExpoView(
83
102
  }
84
103
  }
85
104
 
105
+ fun setEnableLinkTaps(enableLinkTaps: Boolean?) {
106
+ this.enableLinkTaps = enableLinkTaps ?: true
107
+ }
108
+
109
+ fun setInterceptLinkTaps(interceptLinkTaps: Boolean?) {
110
+ this.interceptLinkTaps = interceptLinkTaps ?: false
111
+ }
112
+
86
113
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
87
114
  val childWidthSpec = getChildMeasureSpec(
88
115
  widthMeasureSpec,
@@ -154,4 +181,14 @@ class NativeProseViewerExpoView(
154
181
 
155
182
  return (resources.displayMetrics.widthPixels - paddingLeft - paddingRight).coerceAtLeast(1)
156
183
  }
184
+
185
+ private fun openLink(href: String): Boolean {
186
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(href)).apply {
187
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
188
+ }
189
+ return runCatching {
190
+ context.startActivity(intent)
191
+ true
192
+ }.getOrDefault(false)
193
+ }
157
194
  }
@@ -752,6 +752,7 @@ class CenteredBulletSpan(
752
752
  object RenderBridge {
753
753
  internal const val NATIVE_BLOCKQUOTE_ANNOTATION = "nativeBlockquote"
754
754
  internal const val NATIVE_TOP_LEVEL_CHILD_INDEX_ANNOTATION = "nativeTopLevelChildIndex"
755
+ internal const val NATIVE_LINK_HREF_ANNOTATION = "nativeLinkHref"
755
756
  private const val NATIVE_SYNTHETIC_PLACEHOLDER_ANNOTATION = "nativeSyntheticPlaceholder"
756
757
 
757
758
  private data class RenderBuildState(
@@ -909,6 +910,9 @@ object RenderBridge {
909
910
  val nodeType = element.optString("nodeType", "")
910
911
  val label = element.optString("label", "?")
911
912
  val docPos = element.optInt("docPos", 0)
913
+ val mentionTheme = EditorMentionTheme.fromJson(
914
+ element.optJSONObject("mentionTheme")
915
+ )
912
916
  appendOpaqueInlineAtom(
913
917
  state.result,
914
918
  nodeType,
@@ -919,6 +923,7 @@ object RenderBridge {
919
923
  state.blockStack,
920
924
  state.pendingLeadingMargins,
921
925
  theme,
926
+ mentionTheme,
922
927
  density
923
928
  )
924
929
  }
@@ -1170,6 +1175,15 @@ object RenderBridge {
1170
1175
  end,
1171
1176
  Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1172
1177
  )
1178
+ val href = mark.optString("href", "")
1179
+ if (href.isNotBlank()) {
1180
+ builder.setSpan(
1181
+ Annotation(NATIVE_LINK_HREF_ANNOTATION, href),
1182
+ start,
1183
+ end,
1184
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1185
+ )
1186
+ }
1173
1187
  }
1174
1188
  }
1175
1189
  }
@@ -1353,6 +1367,7 @@ object RenderBridge {
1353
1367
  blockStack: MutableList<BlockContext>,
1354
1368
  pendingLeadingMargins: MutableMap<Int, PendingLeadingMargin>,
1355
1369
  theme: EditorTheme?,
1370
+ mentionTheme: EditorMentionTheme?,
1356
1371
  density: Float
1357
1372
  ) {
1358
1373
  val isMention = nodeType == "mention"
@@ -1360,8 +1375,13 @@ object RenderBridge {
1360
1375
  val start = builder.length
1361
1376
  builder.append(text)
1362
1377
  val end = builder.length
1378
+ val resolvedMentionTheme = if (isMention) {
1379
+ theme?.mentions?.mergedWith(mentionTheme) ?: mentionTheme
1380
+ } else {
1381
+ null
1382
+ }
1363
1383
  val inlineTextColor = if (isMention) {
1364
- theme?.mentions?.textColor ?: resolveInlineTextColor(blockStack, textColor, theme)
1384
+ resolvedMentionTheme?.textColor ?: resolveInlineTextColor(blockStack, textColor, theme)
1365
1385
  } else {
1366
1386
  resolveInlineTextColor(blockStack, textColor, theme)
1367
1387
  }
@@ -1372,7 +1392,7 @@ object RenderBridge {
1372
1392
  builder.setSpan(
1373
1393
  BackgroundColorSpan(
1374
1394
  if (isMention) {
1375
- theme?.mentions?.backgroundColor ?: 0x1f1d4ed8
1395
+ resolvedMentionTheme?.backgroundColor ?: 0x1f1d4ed8
1376
1396
  } else {
1377
1397
  0x20000000
1378
1398
  }
@@ -1387,8 +1407,8 @@ object RenderBridge {
1387
1407
  Annotation("nativeDocPos", docPos.toString()),
1388
1408
  start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
1389
1409
  )
1390
- if (isMention && (theme?.mentions?.fontWeight == "bold" ||
1391
- theme?.mentions?.fontWeight?.toIntOrNull()?.let { it >= 600 } == true)
1410
+ if (isMention && (resolvedMentionTheme?.fontWeight == "bold" ||
1411
+ resolvedMentionTheme?.fontWeight?.toIntOrNull()?.let { it >= 600 } == true)
1392
1412
  ) {
1393
1413
  builder.setSpan(
1394
1414
  StyleSpan(Typeface.BOLD),
@@ -1,3 +1,4 @@
1
+ import type { EditorMentionTheme } from './EditorTheme';
1
2
  import { type SchemaDefinition } from './schemas';
2
3
  export interface NativeEditorModule {
3
4
  editorCreate(configJson: string): number;
@@ -93,6 +94,7 @@ export interface RenderElement {
93
94
  docPos?: number;
94
95
  label?: string;
95
96
  attrs?: Record<string, unknown>;
97
+ mentionTheme?: EditorMentionTheme;
96
98
  listContext?: ListContext;
97
99
  }
98
100
  interface RenderBlocksPatch {
@@ -194,6 +196,8 @@ export declare class NativeEditorBridge {
194
196
  toggleMark(markType: string): EditorUpdate | null;
195
197
  /** Set a mark with attrs on the current selection. */
196
198
  setMark(markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
199
+ /** Set a mark with attrs at an explicit scalar selection. */
200
+ setMarkAtSelectionScalar(scalarAnchor: number, scalarHead: number, markType: string, attrs: Record<string, unknown>): EditorUpdate | null;
197
201
  /** Remove a mark from the current selection. */
198
202
  unsetMark(markType: string): EditorUpdate | null;
199
203
  /** Toggle blockquote wrapping for the current block selection. */
@@ -202,6 +206,10 @@ export declare class NativeEditorBridge {
202
206
  toggleHeading(level: number): EditorUpdate | null;
203
207
  /** Set the document selection by anchor and head positions. */
204
208
  setSelection(anchor: number, head: number): void;
209
+ /** Convert a document position to a scalar position used by native text views. */
210
+ docToScalar(docPos: number): number;
211
+ /** Convert a native scalar position back to a document position. */
212
+ scalarToDoc(scalar: number): number;
205
213
  /** Get the current selection from the Rust engine (synchronous native call).
206
214
  * Always returns the live selection, not a stale cache. */
207
215
  getSelection(): Selection;
@@ -433,6 +433,12 @@ class NativeEditorBridge {
433
433
  : getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
434
434
  return this.parseAndNoteUpdate(json);
435
435
  }
436
+ /** Set a mark with attrs at an explicit scalar selection. */
437
+ setMarkAtSelectionScalar(scalarAnchor, scalarHead, markType, attrs) {
438
+ this.assertNotDestroyed();
439
+ const json = getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, markType, JSON.stringify(attrs));
440
+ return this.parseAndNoteUpdate(json);
441
+ }
436
442
  /** Remove a mark from the current selection. */
437
443
  unsetMark(markType) {
438
444
  this.assertNotDestroyed();
@@ -469,6 +475,16 @@ class NativeEditorBridge {
469
475
  getNativeModule().editorSetSelection(this._editorId, anchor, head);
470
476
  this._lastSelection = { type: 'text', anchor, head };
471
477
  }
478
+ /** Convert a document position to a scalar position used by native text views. */
479
+ docToScalar(docPos) {
480
+ this.assertNotDestroyed();
481
+ return getNativeModule().editorDocToScalar(this._editorId, docPos);
482
+ }
483
+ /** Convert a native scalar position back to a document position. */
484
+ scalarToDoc(scalar) {
485
+ this.assertNotDestroyed();
486
+ return getNativeModule().editorScalarToDoc(this._editorId, scalar);
487
+ }
472
488
  /** Get the current selection from the Rust engine (synchronous native call).
473
489
  * Always returns the live selection, not a stale cache. */
474
490
  getSelection() {
@@ -1,21 +1,41 @@
1
1
  import { type StyleProp, type ViewStyle } from 'react-native';
2
- import { type EditorTheme } from './EditorTheme';
2
+ import { type EditorMentionTheme, type EditorTheme } from './EditorTheme';
3
3
  import type { DocumentJSON } from './NativeEditorBridge';
4
4
  import { type SchemaDefinition } from './schemas';
5
- export interface NativeProseViewerMentionPressEvent {
5
+ export interface NativeProseViewerMentionRenderContext {
6
6
  docPos: number;
7
7
  label: string;
8
8
  attrs: Record<string, unknown>;
9
9
  }
10
+ export interface NativeProseViewerMentionPressEvent extends NativeProseViewerMentionRenderContext {
11
+ }
12
+ export interface NativeProseViewerLinkPressEvent {
13
+ href: string;
14
+ text: string;
15
+ }
10
16
  type NativeProseViewerContent = DocumentJSON | string;
11
- export interface NativeProseViewerProps {
12
- contentJSON: NativeProseViewerContent;
17
+ interface NativeProseViewerBaseProps {
18
+ contentRevision?: string | number;
13
19
  contentJSONRevision?: string | number;
14
20
  schema?: SchemaDefinition;
15
21
  theme?: EditorTheme;
16
22
  style?: StyleProp<ViewStyle>;
17
23
  allowBase64Images?: boolean;
24
+ collapseTrailingEmptyParagraphs?: boolean;
25
+ enableLinkTaps?: boolean;
26
+ mentionPrefix?: string | ((mention: NativeProseViewerMentionRenderContext) => string | null | undefined);
27
+ resolveMentionTheme?: (mention: NativeProseViewerMentionRenderContext) => EditorMentionTheme | null | undefined;
28
+ onPressLink?: (event: NativeProseViewerLinkPressEvent) => void;
18
29
  onPressMention?: (event: NativeProseViewerMentionPressEvent) => void;
19
30
  }
20
- export declare function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images, onPressMention, }: NativeProseViewerProps): import("react/jsx-runtime").JSX.Element;
31
+ interface NativeProseViewerJsonProps extends NativeProseViewerBaseProps {
32
+ contentJSON: NativeProseViewerContent;
33
+ contentHTML?: never;
34
+ }
35
+ interface NativeProseViewerHtmlProps extends NativeProseViewerBaseProps {
36
+ contentHTML: string;
37
+ contentJSON?: never;
38
+ }
39
+ export type NativeProseViewerProps = NativeProseViewerJsonProps | NativeProseViewerHtmlProps;
40
+ export declare function NativeProseViewer({ ...props }: NativeProseViewerProps): import("react/jsx-runtime").JSX.Element;
21
41
  export {};