react-native-enriched-markdown-scaffold 0.6.0-scaffold.8 → 0.7.4-scaffold.10

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 (213) hide show
  1. package/ReactNativeEnrichedMarkdown.podspec +62 -11
  2. package/android/build.gradle +5 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +12 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +4 -0
  7. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  8. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +25 -0
  10. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +793 -0
  11. package/android/src/main/baseline-prof.txt +75 -2
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +10 -2
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +10 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +9 -2
  16. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +10 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +6 -4
  18. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/AccessibilityLabels.kt +15 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +44 -7
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +72 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +110 -12
  22. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +19 -0
  24. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +84 -6
  25. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputRemend.kt +1 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
  27. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +13 -2
  28. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +54 -13
  30. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +17 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +1 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +4 -4
  33. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +8 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
  35. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +5 -1
  36. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
  38. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +37 -7
  39. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/SpannableExtensions.kt +12 -4
  41. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +22 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +36 -7
  43. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +74 -4
  44. package/android/src/main/jni/CMakeLists.txt +15 -1
  45. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +2 -0
  46. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
  47. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt +7 -3
  48. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +8 -5
  49. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
  50. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +27 -3
  51. package/app.plugin.js +2 -1
  52. package/cpp/parser/MD4CParser.cpp +1 -1
  53. package/cpp/wasm/build.sh +9 -9
  54. package/docs/ACCESSIBILITY.md +172 -0
  55. package/docs/API_REFERENCE.md +957 -0
  56. package/docs/COPY_OPTIONS.md +127 -0
  57. package/docs/ELEMENTS_STRUCTURE.md +112 -0
  58. package/docs/IMAGE_CACHING.md +25 -0
  59. package/docs/INPUT.md +281 -0
  60. package/docs/LATEX_MATH.md +198 -0
  61. package/docs/MACOS.md +19 -0
  62. package/docs/MARKDOWN_STREAMING.md +36 -0
  63. package/docs/MENTIONS.md +97 -0
  64. package/docs/RTL.md +82 -0
  65. package/docs/STYLES.md +522 -0
  66. package/docs/TEXT.md +110 -0
  67. package/docs/WEB.md +49 -0
  68. package/ios/EnrichedMarkdown.mm +126 -37
  69. package/ios/EnrichedMarkdownText.mm +84 -16
  70. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
  71. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  72. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  73. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +25 -0
  74. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +793 -0
  75. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +15 -0
  76. package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
  77. package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
  78. package/ios/input/ENRMFormattingStore.h +5 -0
  79. package/ios/input/ENRMFormattingStore.mm +34 -0
  80. package/ios/input/ENRMInputFormatter.h +4 -0
  81. package/ios/input/ENRMInputFormatter.mm +2 -0
  82. package/ios/input/ENRMInputParser.h +5 -0
  83. package/ios/input/ENRMInputParser.mm +79 -6
  84. package/ios/input/ENRMInputTextView.h +2 -0
  85. package/ios/input/ENRMInputTextView.mm +8 -17
  86. package/ios/input/ENRMMarkdownSerializer.mm +1 -1
  87. package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +71 -53
  88. package/ios/input/EnrichedMarkdownTextInput+Internal.h +26 -0
  89. package/ios/input/EnrichedMarkdownTextInput.h +1 -0
  90. package/ios/input/EnrichedMarkdownTextInput.mm +249 -31
  91. package/ios/input/InputStylePropsUtils.h +13 -0
  92. package/ios/input/styles/ENRMBoldStyleHandler.mm +3 -2
  93. package/ios/input/styles/ENRMItalicStyleHandler.mm +3 -2
  94. package/ios/internals/MeasurementCache.h +1 -0
  95. package/ios/math/ENRMRaTeXBridge.swift +52 -3
  96. package/ios/parser/ENRMMarkdownParser.h +8 -0
  97. package/ios/parser/ENRMMarkdownParser.mm +11 -0
  98. package/ios/renderer/AttributedRenderer.m +51 -6
  99. package/ios/renderer/ENRMMathInlineRenderer.m +8 -1
  100. package/ios/renderer/ListItemRenderer.m +8 -5
  101. package/ios/styles/ENRMFontSlot.h +1 -1
  102. package/ios/styles/StyleConfig.h +6 -0
  103. package/ios/styles/StyleConfig.mm +36 -0
  104. package/ios/utils/ENRMAccessibilityLabels.h +36 -0
  105. package/ios/utils/ENRMAccessibilityLabels.m +5 -0
  106. package/ios/utils/ENRMMathFontWeight.h +18 -0
  107. package/ios/utils/EditMenuUtils+macOS.m +4 -5
  108. package/ios/utils/EditMenuUtils.h +70 -0
  109. package/ios/utils/EditMenuUtils.m +14 -11
  110. package/ios/utils/LastElementUtils.h +39 -2
  111. package/ios/utils/MarkdownAccessibilityElementBuilder.h +18 -16
  112. package/ios/utils/MarkdownAccessibilityElementBuilder.m +64 -32
  113. package/ios/utils/ParagraphStyleUtils.m +21 -2
  114. package/ios/utils/StylePropsUtils.h +21 -0
  115. package/ios/views/ENRMMathContainerView.h +8 -0
  116. package/ios/views/ENRMMathContainerView.m +10 -6
  117. package/ios/views/ENRMTableIOSGridView.h +40 -0
  118. package/ios/views/ENRMTableIOSGridView.m +216 -0
  119. package/ios/views/EnrichedMarkdownInternalText.h +2 -0
  120. package/ios/views/EnrichedMarkdownInternalText.m +2 -0
  121. package/ios/views/TableContainerView.h +8 -0
  122. package/ios/views/TableContainerView.m +72 -128
  123. package/lib/module/EnrichedMarkdownNativeComponent.ts +40 -0
  124. package/lib/module/EnrichedMarkdownTextInput.js +50 -1
  125. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  126. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +47 -0
  127. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +40 -0
  128. package/lib/module/accessibilityLabelDefaults.js +64 -0
  129. package/lib/module/accessibilityLabelDefaults.js.map +1 -0
  130. package/lib/module/index.js +1 -0
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/native/EnrichedMarkdownText.js +72 -4
  133. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  134. package/lib/module/normalizeMarkdownStyle.js +5 -2
  135. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  136. package/lib/module/normalizeMarkdownStyle.web.js +5 -2
  137. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  138. package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
  139. package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
  140. package/lib/module/normalizeMenuItem.js +38 -0
  141. package/lib/module/normalizeMenuItem.js.map +1 -0
  142. package/lib/module/types/AccessibilityLabels.js +2 -0
  143. package/lib/module/types/AccessibilityLabels.js.map +1 -0
  144. package/lib/module/web/globalStyles.js +8 -1
  145. package/lib/module/web/globalStyles.js.map +1 -1
  146. package/lib/module/web/styles.js +3 -1
  147. package/lib/module/web/styles.js.map +1 -1
  148. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +37 -0
  149. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  150. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +57 -1
  151. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  152. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +42 -0
  153. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  154. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +37 -0
  155. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  156. package/lib/typescript/src/accessibilityLabelDefaults.d.ts +16 -0
  157. package/lib/typescript/src/accessibilityLabelDefaults.d.ts.map +1 -0
  158. package/lib/typescript/src/index.d.ts +4 -2
  159. package/lib/typescript/src/index.d.ts.map +1 -1
  160. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
  161. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  162. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  163. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  164. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +1 -0
  165. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
  166. package/lib/typescript/src/normalizeMenuItem.d.ts +7 -0
  167. package/lib/typescript/src/normalizeMenuItem.d.ts.map +1 -0
  168. package/lib/typescript/src/types/AccessibilityLabels.d.ts +114 -0
  169. package/lib/typescript/src/types/AccessibilityLabels.d.ts.map +1 -0
  170. package/lib/typescript/src/types/MarkdownStyle.d.ts +32 -0
  171. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  172. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +3 -0
  173. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  174. package/lib/typescript/src/types/MarkdownTextProps.d.ts +78 -9
  175. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  176. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  177. package/package.json +4 -2
  178. package/plugin/build/withAndroidMath.js +19 -0
  179. package/plugin/build/withIosMath.js +28 -0
  180. package/plugin/build/withReactNativeEnrichedMarkdown.js +11 -0
  181. package/plugin/tsconfig.build.json +16 -0
  182. package/src/EnrichedMarkdownNativeComponent.ts +40 -0
  183. package/src/EnrichedMarkdownTextInput.tsx +121 -1
  184. package/src/EnrichedMarkdownTextInputNativeComponent.ts +47 -0
  185. package/src/EnrichedMarkdownTextNativeComponent.ts +40 -0
  186. package/src/accessibilityLabelDefaults.ts +83 -0
  187. package/src/index.tsx +11 -0
  188. package/src/native/EnrichedMarkdownText.tsx +120 -6
  189. package/src/normalizeMarkdownStyle.ts +6 -1
  190. package/src/normalizeMarkdownStyle.web.ts +6 -1
  191. package/src/normalizeMarkdownTextInputStyle.ts +5 -0
  192. package/src/normalizeMenuItem.ts +52 -0
  193. package/src/types/AccessibilityLabels.ts +118 -0
  194. package/src/types/MarkdownStyle.ts +32 -0
  195. package/src/types/MarkdownStyleInternal.ts +3 -0
  196. package/src/types/MarkdownTextProps.ts +74 -9
  197. package/src/web/globalStyles.ts +7 -0
  198. package/src/web/styles.ts +5 -1
  199. package/lib/module/plugin/withAndroidMath.js +0 -20
  200. package/lib/module/plugin/withAndroidMath.js.map +0 -1
  201. package/lib/module/plugin/withIosMath.js +0 -23
  202. package/lib/module/plugin/withIosMath.js.map +0 -1
  203. package/lib/module/plugin/withReactNativeEnrichedMarkdown.js +0 -16
  204. package/lib/module/plugin/withReactNativeEnrichedMarkdown.js.map +0 -1
  205. package/lib/typescript/src/plugin/withAndroidMath.d.ts +0 -5
  206. package/lib/typescript/src/plugin/withAndroidMath.d.ts.map +0 -1
  207. package/lib/typescript/src/plugin/withIosMath.d.ts +0 -5
  208. package/lib/typescript/src/plugin/withIosMath.d.ts.map +0 -1
  209. package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts +0 -6
  210. package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts.map +0 -1
  211. /package/{src/plugin → plugin/src}/withAndroidMath.ts +0 -0
  212. /package/{src/plugin → plugin/src}/withIosMath.ts +0 -0
  213. /package/{src/plugin → plugin/src}/withReactNativeEnrichedMarkdown.ts +0 -0
@@ -32,6 +32,8 @@ import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
32
32
  import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
33
33
  import com.swmansion.enriched.markdown.input.layout.InputMeasurementStore
34
34
  import com.swmansion.enriched.markdown.input.model.StyleType
35
+ import com.swmansion.enriched.markdown.input.toolbar.FormatMenuConfig
36
+ import com.swmansion.enriched.markdown.input.toolbar.InputSelectionMenuConfig
35
37
  import com.swmansion.enriched.markdown.utils.input.BorderPropsApplicator
36
38
  import com.swmansion.enriched.markdown.utils.input.MarkdownStyleParser
37
39
 
@@ -55,6 +57,13 @@ class EnrichedMarkdownTextInputManager :
55
57
  stateWrapper: StateWrapper?,
56
58
  ): Any? {
57
59
  view.stateWrapper = stateWrapper
60
+ // Fabric applies props before it hands over the state wrapper, so every layout
61
+ // invalidation raised during the mount transaction was dropped. Replay it now that we can
62
+ // actually push state — otherwise the shadow node keeps the initialMeasure() estimate,
63
+ // which ignores math chips, until the first keystroke.
64
+ if (stateWrapper != null) {
65
+ view.layoutManager.flushPendingInvalidation()
66
+ }
58
67
  return super.updateState(view, props, stateWrapper)
59
68
  }
60
69
 
@@ -67,6 +76,11 @@ class EnrichedMarkdownTextInputManager :
67
76
  view.dismissActiveMention()
68
77
  super.onDropViewInstance(view)
69
78
  view.layoutManager.release()
79
+ // Unreachable while view recycling is off — it needs a `setupViewRecycling()` call from this
80
+ // constructor *and* ReactNativeFeatureFlags.enableViewRecycling(). Kept because the flush
81
+ // above turns a recycled view into a view that pushes state at mount: without this, it would
82
+ // push into the shadow node it was dropped from.
83
+ view.stateWrapper = null
70
84
  }
71
85
 
72
86
  override fun measure(
@@ -156,6 +170,14 @@ class EnrichedMarkdownTextInputManager :
156
170
  view?.isVerticalScrollBarEnabled = value
157
171
  }
158
172
 
173
+ @ReactProp(name = "blockSyntax", defaultBoolean = true)
174
+ override fun setBlockSyntax(
175
+ view: EnrichedMarkdownTextInputView?,
176
+ value: Boolean,
177
+ ) {
178
+ view?.blockSyntax = value
179
+ }
180
+
159
181
  @ReactProp(name = "autoCapitalize")
160
182
  override fun setAutoCapitalize(
161
183
  view: EnrichedMarkdownTextInputView?,
@@ -266,6 +288,52 @@ class EnrichedMarkdownTextInputManager :
266
288
  view.setContextMenuItems(items)
267
289
  }
268
290
 
291
+ @ReactProp(name = "selectionMenuConfig")
292
+ override fun setSelectionMenuConfig(
293
+ view: EnrichedMarkdownTextInputView?,
294
+ value: ReadableMap?,
295
+ ) {
296
+ if (view == null) return
297
+ view.contextMenu.selectionMenuConfig =
298
+ if (value == null) {
299
+ InputSelectionMenuConfig()
300
+ } else {
301
+ InputSelectionMenuConfig(
302
+ format = value.getBoolean("format"),
303
+ formatLabel = value.getString("formatLabel") ?: "",
304
+ copyAsMarkdown = value.getBoolean("copyAsMarkdown"),
305
+ copyAsMarkdownLabel = value.getString("copyAsMarkdownLabel") ?: "",
306
+ )
307
+ }
308
+ }
309
+
310
+ @ReactProp(name = "formatMenuConfig")
311
+ override fun setFormatMenuConfig(
312
+ view: EnrichedMarkdownTextInputView?,
313
+ value: ReadableMap?,
314
+ ) {
315
+ if (view == null) return
316
+ view.contextMenu.formatMenuConfig =
317
+ if (value == null) {
318
+ FormatMenuConfig()
319
+ } else {
320
+ FormatMenuConfig(
321
+ bold = value.getBoolean("bold"),
322
+ boldLabel = value.getString("boldLabel") ?: "",
323
+ italic = value.getBoolean("italic"),
324
+ italicLabel = value.getString("italicLabel") ?: "",
325
+ underline = value.getBoolean("underline"),
326
+ underlineLabel = value.getString("underlineLabel") ?: "",
327
+ strikethrough = value.getBoolean("strikethrough"),
328
+ strikethroughLabel = value.getString("strikethroughLabel") ?: "",
329
+ spoiler = value.getBoolean("spoiler"),
330
+ spoilerLabel = value.getString("spoilerLabel") ?: "",
331
+ link = value.getBoolean("link"),
332
+ linkLabel = value.getString("linkLabel") ?: "",
333
+ )
334
+ }
335
+ }
336
+
269
337
  @ReactProp(name = "linkRegex")
270
338
  override fun setLinkRegex(
271
339
  view: EnrichedMarkdownTextInputView?,
@@ -445,6 +513,10 @@ class EnrichedMarkdownTextInputManager :
445
513
  view?.removeMath(start)
446
514
  }
447
515
 
516
+ override fun copyToClipboard(view: EnrichedMarkdownTextInputView?) {
517
+ view?.copyToClipboard()
518
+ }
519
+
448
520
  override fun requestMarkdown(
449
521
  view: EnrichedMarkdownTextInputView?,
450
522
  requestId: Int,
@@ -1,5 +1,7 @@
1
1
  package com.swmansion.enriched.markdown.input
2
2
 
3
+ import android.content.ClipData
4
+ import android.content.ClipboardManager
3
5
  import android.content.Context
4
6
  import android.graphics.BlendMode
5
7
  import android.graphics.BlendModeColorFilter
@@ -44,6 +46,8 @@ import com.swmansion.enriched.markdown.input.toolbar.InputContextMenu
44
46
  import com.swmansion.enriched.markdown.utils.input.AutoCapitalizeUtils
45
47
  import kotlin.math.ceil
46
48
 
49
+ private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r' || this == '\u0085' || this == '\u2028' || this == '\u2029'
50
+
47
51
  class EnrichedMarkdownTextInputView(
48
52
  context: Context,
49
53
  ) : AppCompatEditText(context) {
@@ -71,6 +75,7 @@ class EnrichedMarkdownTextInputView(
71
75
  var autoFocusRequested = false
72
76
  var stateWrapper: StateWrapper? = null
73
77
  val layoutManager = InputLayoutManager(this)
78
+ private var pendingAutoFocusKeyboard = false
74
79
 
75
80
  private var typefaceDirty = false
76
81
  private var fontFamilyValue: String? = null
@@ -92,6 +97,17 @@ class EnrichedMarkdownTextInputView(
92
97
  private var detectScrollMovement = false
93
98
  var scrollEnabled: Boolean = true
94
99
 
100
+ // The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
101
+ // setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
102
+ private var lastMarkdownFromJS: String? = null
103
+
104
+ var blockSyntax: Boolean = true
105
+ set(value) {
106
+ if (field == value) return
107
+ field = value
108
+ lastMarkdownFromJS?.let { setValueFromJS(it) }
109
+ }
110
+
95
111
  private var mentionIndicators: LinkedHashSet<String> = linkedSetOf()
96
112
  private var activeMentionIndicator: String? = null
97
113
  private var activeMentionStart = -1
@@ -176,6 +192,13 @@ class EnrichedMarkdownTextInputView(
176
192
  refreshAllMathChips()
177
193
  }
178
194
 
195
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
196
+ super.onWindowFocusChanged(hasWindowFocus)
197
+ // The autofocus keyboard request may run before the window has IME focus (e.g. while a modal is
198
+ // still presenting), where showSoftInput() is dropped.
199
+ showAutoFocusKeyboardIfPending()
200
+ }
201
+
179
202
  override fun clearFocus() {
180
203
  super.clearFocus()
181
204
  inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
@@ -312,6 +335,12 @@ class EnrichedMarkdownTextInputView(
312
335
  if (!isComponentReady || isDuringTransaction) return
313
336
 
314
337
  if (!isTextChanging) {
338
+ // Links (e.g. mentions) are atomic: snap a partial selection to the whole link, and a caret
339
+ // inside a link to its end. Returning lets the recursive onSelectionChanged emit for the result.
340
+ formattingStore.selectionAdjustedForAtomicLinks(selStart, selEnd)?.let { (newStart, newEnd) ->
341
+ setSelection(newStart, newEnd)
342
+ return
343
+ }
315
344
  if (didTextChangeRecently) {
316
345
  didTextChangeRecently = false
317
346
  } else {
@@ -345,8 +374,18 @@ class EnrichedMarkdownTextInputView(
345
374
  val rangeStart = if (preEditSelectionStart != preEditSelectionEnd) preEditSelectionStart else editStart
346
375
  val rangeEnd = rangeStart + insertedLength
347
376
 
348
- for (style in pendingStyles) {
349
- formattingStore.addRange(FormattingRange(style, rangeStart, rangeEnd))
377
+ // Skip applying pending styles when the insertion is only line breaks —
378
+ // a phantom range over a bare newline corrupts isStyleActive() at the boundary.
379
+ val currentText = text
380
+ val insertedHasGlyphContent =
381
+ currentText != null &&
382
+ rangeEnd <= currentText.length &&
383
+ (rangeStart until rangeEnd).any { !currentText[it].isLineBreak() }
384
+
385
+ if (insertedHasGlyphContent) {
386
+ for (style in pendingStyles) {
387
+ formattingStore.addRange(FormattingRange(style, rangeStart, rangeEnd))
388
+ }
350
389
  }
351
390
 
352
391
  for (style in pendingStyleRemovals) {
@@ -439,6 +478,16 @@ class EnrichedMarkdownTextInputView(
439
478
  }
440
479
  }
441
480
 
481
+ // Copies the input's plain text to the system clipboard without disturbing
482
+ // the current selection. Android's system clipboard round-trip is plain text
483
+ // only — inline styles are not preserved for external paste targets.
484
+ fun copyToClipboard() {
485
+ val content = text
486
+ if (content.isNullOrEmpty()) return
487
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
488
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, content))
489
+ }
490
+
442
491
  fun setLinkForSelection(url: String) {
443
492
  val selStart = selectionStart
444
493
  val selEnd = selectionEnd
@@ -596,7 +645,8 @@ class EnrichedMarkdownTextInputView(
596
645
  }
597
646
 
598
647
  fun setValueFromJS(markdown: String) {
599
- val parsed = InputParser.parseToPlainTextAndRanges(markdown)
648
+ lastMarkdownFromJS = markdown
649
+ val parsed = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax)
600
650
  blockEmitting = true
601
651
  try {
602
652
  runAsATransaction {
@@ -670,8 +720,16 @@ class EnrichedMarkdownTextInputView(
670
720
  }
671
721
  invalidate()
672
722
  requestLayout()
723
+ // Chips only reach the text here when `defaultValue` landed before `markdownStyle`
724
+ // (@ReactProp order is undefined). requestLayout() cannot change a height Fabric owns —
725
+ // the shadow tree has to re-measure text that now carries the spans.
726
+ layoutManager.invalidateLayout()
673
727
  } else if (mathStyleChanged) {
674
728
  refreshAllMathChips()
729
+ // A math font size / padding change resizes every chip, so the measured height moves.
730
+ // Colour-only changes end up here too; store() then measures the same size and skips
731
+ // the state update.
732
+ layoutManager.invalidateLayout()
675
733
  }
676
734
  return changed
677
735
  }
@@ -810,18 +868,36 @@ class EnrichedMarkdownTextInputView(
810
868
  return true
811
869
  }
812
870
 
813
- private fun computeChipBounds(
871
+ /** Chip glyph rect in [Layout] coordinates — no padding, no scroll offset. */
872
+ private fun chipBoundsInLayout(
814
873
  layout: Layout,
815
874
  start: Int,
816
875
  ): Rect {
817
876
  val line = layout.getLineForOffset(start)
818
- val xStart = (layout.getPrimaryHorizontal(start)).toInt() + totalPaddingLeft - scrollX
819
- val xEnd = (layout.getPrimaryHorizontal(start + 1)).toInt() + totalPaddingLeft - scrollX
820
- val top = layout.getLineTop(line) + totalPaddingTop - scrollY
821
- val bottom = layout.getLineBottom(line) + totalPaddingTop - scrollY
822
- return Rect(xStart, top, xEnd, bottom)
877
+ val xStart = layout.getPrimaryHorizontal(start).toInt()
878
+ val xEnd = layout.getPrimaryHorizontal(start + 1).toInt()
879
+ // Ordered, not raw start/end: in an RTL run xStart > xEnd, which would make an
880
+ // inverted Rect that contains() always rejects.
881
+ val left = xStart.coerceAtMost(xEnd)
882
+ var right = xStart.coerceAtLeast(xEnd)
883
+ if (right <= left) {
884
+ // A chip that is the last character has start + 1 == length, where some layouts
885
+ // report the same horizontal for both offsets. Fall back to the line's content edge
886
+ // so the rect keeps a real width instead of collapsing.
887
+ right = layout.getLineRight(line).toInt()
888
+ }
889
+ return Rect(left, layout.getLineTop(line), right, layout.getLineBottom(line))
823
890
  }
824
891
 
892
+ /** Chip glyph rect in view coordinates, for reporting to JS. */
893
+ private fun computeChipBounds(
894
+ layout: Layout,
895
+ start: Int,
896
+ ): Rect =
897
+ chipBoundsInLayout(layout, start).apply {
898
+ offset(totalPaddingLeft - scrollX, totalPaddingTop - scrollY)
899
+ }
900
+
825
901
  fun mathChipPositionAtTouch(
826
902
  x: Float,
827
903
  y: Float,
@@ -831,13 +907,21 @@ class EnrichedMarkdownTextInputView(
831
907
  val layoutNow = layout ?: return -1
832
908
  val adjustedX = (x - totalPaddingLeft + scrollX).toInt()
833
909
  val adjustedY = (y - totalPaddingTop + scrollY).toInt()
834
- if (adjustedY < 0) return -1
910
+ // getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text, so
911
+ // a touch in the padding or in the blank space below the last line resolves onto the
912
+ // nearest character. Bound the touch to the text box, then require it to land inside
913
+ // the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
914
+ if (adjustedY < 0 || adjustedY > layoutNow.height) return -1
835
915
  val line = layoutNow.getLineForVertical(adjustedY)
836
916
  val offset = layoutNow.getOffsetForHorizontal(line, adjustedX.toFloat())
837
- if (offset < 0 || offset >= editable.length) return -1
917
+ if (offset < 0) return -1
838
918
  val candidates = intArrayOf(offset, offset - 1).filter { it in 0 until editable.length }
839
919
  for (candidate in candidates) {
840
920
  val range = controller.chipRangeAt(editable, candidate) ?: continue
921
+ // An empty rect means the layout could not give us the chip's extent; accept the
922
+ // candidate then, rather than making the chip untappable.
923
+ val bounds = chipBoundsInLayout(layoutNow, range.first)
924
+ if (!bounds.isEmpty && !bounds.contains(adjustedX, adjustedY)) continue
841
925
  return range.first
842
926
  }
843
927
  return -1
@@ -954,11 +1038,25 @@ class EnrichedMarkdownTextInputView(
954
1038
  setSelection(selectionStart.coerceAtLeast(0))
955
1039
  }
956
1040
 
1041
+ private fun showAutoFocusKeyboardIfPending() {
1042
+ if (!pendingAutoFocusKeyboard || !hasWindowFocus()) return
1043
+ pendingAutoFocusKeyboard = false
1044
+ inputMethodManager?.showSoftInput(this, 0)
1045
+ }
1046
+
957
1047
  fun afterUpdateTransaction() {
958
1048
  updateTypeface()
959
1049
  if (autoFocusRequested) {
960
1050
  autoFocusRequested = false
961
- requestFocusProgrammatically()
1051
+ pendingAutoFocusKeyboard = true
1052
+ post {
1053
+ // afterUpdateTransaction runs before onAttachedToWindow, where requestFocus()/showSoftInput()
1054
+ // are dropped and setTextIsSelectable(true) would reset the caret to 0. Defer to the next loop
1055
+ // so focus sticks and the caret lands at end (matching iOS).
1056
+ requestFocus()
1057
+ setSelection(text?.length ?: 0)
1058
+ showAutoFocusKeyboardIfPending()
1059
+ }
962
1060
  }
963
1061
  }
964
1062
 
@@ -0,0 +1,69 @@
1
+ package com.swmansion.enriched.markdown.input.formatting
2
+
3
+ /**
4
+ * Hides block-level markdown structure from md4c so a document parses as a single
5
+ * `Paragraph`. Used by `blockSyntax = false` on `EnrichedMarkdownTextInput`.
6
+ *
7
+ * Every newline is swapped for a vertical tab (`0x0B`) and one more is prepended.
8
+ * md4c treats VT as ordinary whitespace, never as a line break, so no line ever
9
+ * starts at a position where a list marker, `#`, `>` or an indented code block can
10
+ * be recognised — including line 1, which is what the leading sentinel is for.
11
+ * Inline syntax (`$...$`, `**...**`, links) still parses normally.
12
+ *
13
+ * The transform is 1:1 in characters *and* in UTF-8 bytes (VT and `\n` are both one
14
+ * of each), so offsets coming back out of the parser only need a flat -1 shift for
15
+ * the leading sentinel. iOS relies on the same property to keep its byte-to-UTF16
16
+ * map valid.
17
+ */
18
+ internal object BlockSyntaxSentinel {
19
+ const val SENTINEL = '\u000B'
20
+
21
+ /**
22
+ * `\r\n` / `\r` collapse to `\n` (md4c normalizes line endings this way regardless,
23
+ * so this is pre-existing lossiness, not a new one), and any `0x0B` already present
24
+ * is dropped — leaving it in would make [decode] silently turn it into a newline.
25
+ */
26
+ fun canonicalize(markdown: String): String {
27
+ val out = StringBuilder(markdown.length)
28
+ var i = 0
29
+ while (i < markdown.length) {
30
+ when (markdown[i]) {
31
+ '\r' -> {
32
+ out.append('\n')
33
+ if (i + 1 < markdown.length && markdown[i + 1] == '\n') i++
34
+ }
35
+
36
+ SENTINEL -> {}
37
+
38
+ else -> {
39
+ out.append(markdown[i])
40
+ }
41
+ }
42
+ i++
43
+ }
44
+ return out.toString()
45
+ }
46
+
47
+ /** Prepends one sentinel and replaces every `\n` with one. Length grows by exactly 1. */
48
+ fun encode(canonical: String): String {
49
+ val out = StringBuilder(canonical.length + 1)
50
+ out.append(SENTINEL)
51
+ for (c in canonical) out.append(if (c == '\n') SENTINEL else c)
52
+ return out.toString()
53
+ }
54
+
55
+ /**
56
+ * Reverses [encode] on the parser's plain text. Returns `null` when the leading
57
+ * sentinel is missing — the caller then falls back to the canonical source with no
58
+ * ranges rather than emitting text shifted by an offset that no longer holds.
59
+ */
60
+ fun decode(plainText: String): String? {
61
+ if (plainText.isEmpty() || plainText[0] != SENTINEL) return null
62
+ val out = StringBuilder(plainText.length - 1)
63
+ for (i in 1 until plainText.length) {
64
+ val c = plainText[i]
65
+ out.append(if (c == SENTINEL) '\n' else c)
66
+ }
67
+ return out.toString()
68
+ }
69
+ }
@@ -34,6 +34,25 @@ class FormattingStore {
34
34
  end: Int,
35
35
  ): Boolean = ranges.any { it.type == type && it.start < end && it.end > start }
36
36
 
37
+ /**
38
+ * Snaps a selection so it never partially overlaps an atomic link: a partial selection expands to
39
+ * the whole link, a caret inside a link moves to its end. Returns the adjusted (start, end) or null.
40
+ */
41
+ fun selectionAdjustedForAtomicLinks(
42
+ start: Int,
43
+ end: Int,
44
+ ): Pair<Int, Int>? {
45
+ if (start != end) {
46
+ var newStart = start
47
+ var newEnd = end
48
+ rangeOfType(StyleType.LINK, newStart)?.let { newStart = minOf(newStart, it.start) }
49
+ if (newEnd > 0) rangeOfType(StyleType.LINK, newEnd - 1)?.let { newEnd = maxOf(newEnd, it.end) }
50
+ return if (newStart != start || newEnd != end) Pair(newStart, newEnd) else null
51
+ }
52
+ val link = rangeOfType(StyleType.LINK, start)
53
+ return if (link != null && start > link.start && start < link.end) Pair(link.end, link.end) else null
54
+ }
55
+
37
56
  fun addRange(newRange: FormattingRange) {
38
57
  var mergedStart = newRange.start
39
58
  var mergedEnd = newRange.end
@@ -7,6 +7,7 @@ import com.swmansion.enriched.markdown.parser.MarkdownASTNode
7
7
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode.NodeType
8
8
  import com.swmansion.enriched.markdown.parser.Md4cFlags
9
9
  import com.swmansion.enriched.markdown.parser.Parser
10
+ import com.swmansion.enriched.markdown.parser.isTopLevelBlock
10
11
  import com.swmansion.enriched.markdown.utils.common.FeatureFlags
11
12
 
12
13
  data class ParseResult(
@@ -18,25 +19,96 @@ data class ParseResult(
18
19
  object InputParser {
19
20
  private const val ATTACHMENT_CHAR = ''
20
21
 
21
- fun parseToPlainTextAndRanges(markdown: String): ParseResult {
22
+ // A source paragraph break: a newline followed by one or more blank lines.
23
+ private val PARAGRAPH_BREAK_RE = Regex("\\n[ \\t]*(?:\\n[ \\t]*)+")
24
+
25
+ fun parseToPlainTextAndRanges(
26
+ markdown: String,
27
+ blockSyntax: Boolean = true,
28
+ ): ParseResult {
22
29
  if (markdown.isEmpty()) {
23
30
  return ParseResult("", emptyList())
24
31
  }
25
32
 
26
- val completed = InputRemend.complete(markdown)
33
+ if (!blockSyntax) {
34
+ return parseWithoutBlockSyntax(markdown)
35
+ }
36
+
37
+ return parseCompleted(InputRemend.complete(markdown), markdown, blockSeparators = true)
38
+ }
39
+
40
+ /**
41
+ * Parses with every block construct hidden behind [BlockSyntaxSentinel], so the whole
42
+ * document lands in one `Paragraph` and list markers, `#`, `>` and friends stay in the
43
+ * text verbatim. `InputRemend` is skipped as well: its speculative closing delimiters
44
+ * are the other source of characters the serializer cannot replay.
45
+ */
46
+ private fun parseWithoutBlockSyntax(markdown: String): ParseResult {
47
+ val canonical = BlockSyntaxSentinel.canonicalize(markdown)
48
+ if (canonical.isEmpty()) {
49
+ return ParseResult("", emptyList())
50
+ }
51
+
52
+ // No block separators: the encoded text has no newline for md4c to drop, so every
53
+ // newline the replay would add is spurious. `$$...$$` is the case that proves it —
54
+ // md4c reports display math as a top-level block even mid-line under the sentinel.
55
+ val parsed = parseCompleted(BlockSyntaxSentinel.encode(canonical), canonical, blockSeparators = false)
56
+ val decoded =
57
+ BlockSyntaxSentinel.decode(parsed.plainText)
58
+ ?: return ParseResult(canonical, emptyList())
59
+
60
+ // Only the leading sentinel is removed, so every offset shifts by exactly one.
61
+ val ranges =
62
+ parsed.formattingRanges.mapNotNull { range ->
63
+ val start = range.start - 1
64
+ val end = range.end - 1
65
+ if (start < 0 || end > decoded.length || end <= start) {
66
+ null
67
+ } else {
68
+ FormattingRange(range.type, start, end, range.url)
69
+ }
70
+ }
71
+ val atoms =
72
+ parsed.mathAtoms.mapNotNull { atom ->
73
+ val position = atom.position - 1
74
+ if (position < 0 || position >= decoded.length) null else atom.copy(position = position)
75
+ }
76
+
77
+ return ParseResult(decoded, ranges, atoms)
78
+ }
79
+
80
+ private fun parseCompleted(
81
+ completed: String,
82
+ fallback: String,
83
+ blockSeparators: Boolean,
84
+ ): ParseResult {
27
85
  val flags =
28
86
  Md4cFlags(
29
87
  underline = true,
30
88
  latexMath = FeatureFlags.IS_MATH_ENABLED,
31
89
  permissiveAutolinks = false,
32
90
  )
33
- val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(markdown, emptyList())
91
+ val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(fallback, emptyList())
34
92
 
35
93
  val plainText = StringBuilder()
36
94
  val ranges = mutableListOf<FormattingRange>()
37
95
  val mathAtoms = mutableListOf<MathAtom>()
38
96
 
39
- walkNode(ast, plainText, ranges, mathAtoms, ArrayDeque())
97
+ // md4c collapses any blank-line run into a single break, unlike iOS which keeps them.
98
+ // Re-read the real runs so each break replays its original number of newlines.
99
+ val blankRuns =
100
+ if (blockSeparators) {
101
+ ArrayDeque(
102
+ PARAGRAPH_BREAK_RE
103
+ .findAll(completed.trim())
104
+ .map { match -> match.value.count { it == '\n' } }
105
+ .toList(),
106
+ )
107
+ } else {
108
+ ArrayDeque()
109
+ }
110
+
111
+ walkNode(ast, plainText, ranges, mathAtoms, ArrayDeque(), blankRuns, blockSeparators)
40
112
 
41
113
  return ParseResult(plainText.toString(), ranges, mathAtoms)
42
114
  }
@@ -47,6 +119,8 @@ object InputParser {
47
119
  ranges: MutableList<FormattingRange>,
48
120
  mathAtoms: MutableList<MathAtom>,
49
121
  activeStyles: ArrayDeque<ActiveStyle>,
122
+ blankRuns: ArrayDeque<Int>,
123
+ blockSeparators: Boolean,
50
124
  ) {
51
125
  if (node.type == NodeType.LatexMathInline || node.type == NodeType.LatexMathDisplay) {
52
126
  val latex = extractLatex(node)
@@ -75,8 +149,12 @@ object InputParser {
75
149
  plainText.append("\n")
76
150
  }
77
151
 
78
- for (child in node.children) {
79
- walkNode(child, plainText, ranges, mathAtoms, activeStyles)
152
+ for ((index, child) in node.children.withIndex()) {
153
+ // Keep the source's blank lines between top-level blocks (md4c drops them, iOS keeps them).
154
+ if (blockSeparators && index > 0 && child.type.isTopLevelBlock() && plainText.isNotEmpty()) {
155
+ plainText.append("\n".repeat(blankRuns.removeFirstOrNull() ?: 2))
156
+ }
157
+ walkNode(child, plainText, ranges, mathAtoms, activeStyles, blankRuns, blockSeparators)
80
158
  }
81
159
 
82
160
  if (styleType != null) {
@@ -9,6 +9,7 @@ object InputRemend {
9
9
 
10
10
  private val DELIMITER_PAIRS =
11
11
  arrayOf(
12
+ DelimiterPair("***", "***", true),
12
13
  DelimiterPair("**", "**", true),
13
14
  DelimiterPair("*", "*", true),
14
15
  DelimiterPair("_", "_", true),
@@ -8,21 +8,49 @@ class InputLayoutManager(
8
8
  ) {
9
9
  private var forceHeightRecalculationCounter = 0
10
10
 
11
+ /**
12
+ * An invalidation that arrived before `stateWrapper` existed. Fabric applies every prop
13
+ * (and runs `onAfterUpdateTransaction`) inside `ViewManager.updateProperties`, which runs
14
+ * *before* `updateState` assigns the wrapper — so the whole mount transaction invalidates
15
+ * into the void. Without this flag the measurement store stays empty and Yoga keeps the
16
+ * `initialMeasure()` approximation until the next text/prop change.
17
+ */
18
+ private var pending = false
19
+
11
20
  fun invalidateLayout() {
12
- if (view.stateWrapper == null) return
21
+ val stateWrapper = view.stateWrapper
22
+ if (stateWrapper == null) {
23
+ pending = true
24
+ return
25
+ }
26
+ pending = false
13
27
 
14
28
  val text = view.text
15
29
  val paint = view.paint
16
30
 
31
+ // First call for this view measures against cachedWidth = 0 and stores a throwaway size;
32
+ // Yoga immediately re-measures through getMeasureById() with the real width and overwrites
33
+ // it. The state update below is what makes that second pass happen.
17
34
  val needUpdate = InputMeasurementStore.store(view.id, text, paint)
18
35
  if (!needUpdate) return
19
36
 
20
37
  val state = Arguments.createMap()
21
38
  state.putInt("forceHeightRecalculationCounter", forceHeightRecalculationCounter++)
22
- view.stateWrapper?.updateState(state)
39
+ stateWrapper.updateState(state)
40
+ }
41
+
42
+ /**
43
+ * Replays an invalidation that was dropped while `stateWrapper` was null. Called once the
44
+ * wrapper is assigned, i.e. after every prop of the mount transaction has been applied — so
45
+ * the text carries its math chips and the paint carries the resolved typeface/size.
46
+ */
47
+ fun flushPendingInvalidation() {
48
+ if (!pending) return
49
+ invalidateLayout()
23
50
  }
24
51
 
25
52
  fun release() {
53
+ pending = false
26
54
  InputMeasurementStore.release(view.id)
27
55
  }
28
56
  }
@@ -9,6 +9,9 @@ import com.facebook.react.bridge.ReadableMap
9
9
  import com.facebook.react.uimanager.PixelUtil
10
10
  import com.facebook.yoga.YogaMeasureMode
11
11
  import com.facebook.yoga.YogaMeasureOutput
12
+ import com.swmansion.enriched.markdown.input.formatting.InputParser
13
+ import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault
14
+ import com.swmansion.enriched.markdown.utils.common.getFloatOrDefault
12
15
  import java.util.concurrent.ConcurrentHashMap
13
16
 
14
17
  object InputMeasurementStore {
@@ -93,8 +96,16 @@ object InputMeasurementStore {
93
96
  width: Float,
94
97
  props: ReadableMap?,
95
98
  ): Long {
96
- val text = props?.getString("defaultValue") ?: props?.getString("placeholder") ?: "I"
97
- val fontSize = props?.getDouble("fontSize")?.toFloat() ?: 16f
99
+ // Measure the rendered plain text, not the raw markdown. A mention link such as
100
+ // [Label](placeholder://x) hides its URL in the source, so measuring the markdown counts those
101
+ // invisible characters and over-estimates the height into extra lines, until the next text
102
+ // change forces a re-measure. Parsing first matches what the editor actually renders.
103
+ val markdown = props?.getString("defaultValue") ?: props?.getString("placeholder") ?: "I"
104
+ // Must read the same flag the view parses with, or the measured height and the
105
+ // rendered height disagree: `blockSyntax = false` keeps list markers and `#` as text.
106
+ val blockSyntax = props.getBooleanOrDefault("blockSyntax", true)
107
+ val text = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax).plainText
108
+ val fontSize = props.getFloatOrDefault("fontSize", 16f)
98
109
  val spSize = kotlin.math.ceil(PixelUtil.toPixelFromSP(fontSize).toDouble()).toFloat()
99
110
 
100
111
  val defaultView = AppCompatEditText(context)
@@ -13,6 +13,7 @@ open class MathChipStyle(
13
13
  val paddingVertical: Float,
14
14
  val selectedBackgroundColor: Int?,
15
15
  val selectedBorderColor: Int?,
16
+ val fontWeight: String = "normal",
16
17
  ) {
17
18
  open fun isBlock(): Boolean = false
18
19
  }
@@ -28,6 +29,7 @@ class MathBlockStyle(
28
29
  paddingVertical: Float,
29
30
  selectedBackgroundColor: Int?,
30
31
  selectedBorderColor: Int?,
32
+ fontWeight: String = "normal",
31
33
  val marginTop: Float,
32
34
  val marginBottom: Float,
33
35
  val alignment: Layout.Alignment,
@@ -42,6 +44,7 @@ class MathBlockStyle(
42
44
  paddingVertical,
43
45
  selectedBackgroundColor,
44
46
  selectedBorderColor,
47
+ fontWeight,
45
48
  ) {
46
49
  override fun isBlock(): Boolean = true
47
50
  }