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
@@ -14,12 +14,34 @@ import com.swmansion.enriched.markdown.input.formatting.MarkdownSerializer
14
14
  import com.swmansion.enriched.markdown.input.model.FormattingRange
15
15
  import com.swmansion.enriched.markdown.input.model.StyleType
16
16
 
17
- // TODO: Wrap all user-facing strings for localization support.
17
+ data class InputSelectionMenuConfig(
18
+ val format: Boolean = true,
19
+ val formatLabel: String = "",
20
+ val copyAsMarkdown: Boolean = true,
21
+ val copyAsMarkdownLabel: String = "",
22
+ )
23
+
24
+ data class FormatMenuConfig(
25
+ val bold: Boolean = true,
26
+ val boldLabel: String = "",
27
+ val italic: Boolean = true,
28
+ val italicLabel: String = "",
29
+ val underline: Boolean = true,
30
+ val underlineLabel: String = "",
31
+ val strikethrough: Boolean = true,
32
+ val strikethroughLabel: String = "",
33
+ val spoiler: Boolean = true,
34
+ val spoilerLabel: String = "",
35
+ val link: Boolean = true,
36
+ val linkLabel: String = "",
37
+ )
18
38
 
19
39
  class InputContextMenu(
20
40
  private val view: EnrichedMarkdownTextInputView,
21
41
  ) {
22
42
  private var customItemTexts: List<String> = emptyList()
43
+ var selectionMenuConfig: InputSelectionMenuConfig = InputSelectionMenuConfig()
44
+ var formatMenuConfig: FormatMenuConfig = FormatMenuConfig()
23
45
 
24
46
  fun setContextMenuItems(items: List<String>) {
25
47
  customItemTexts = items
@@ -42,13 +64,20 @@ class InputContextMenu(
42
64
  menu.removeGroup(FORMAT_MENU_GROUP_ID)
43
65
  menu.removeGroup(CUSTOM_MENU_GROUP_ID)
44
66
 
45
- val formatSubMenu = menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, "Format")
46
- FORMAT_ITEMS.forEachIndexed { index, (title, _) ->
47
- formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, title)
67
+ if (selectionMenuConfig.format) {
68
+ val formatSubMenu =
69
+ menu.addSubMenu(FORMAT_MENU_GROUP_ID, MENU_FORMAT_ID, 100, selectionMenuConfig.formatLabel)
70
+ FORMAT_ITEMS.forEachIndexed { index, (styleType, labelOf) ->
71
+ if (isFormatItemVisible(styleType)) {
72
+ formatSubMenu.add(Menu.NONE, MENU_FORMAT_ITEM_BASE + index, index, labelOf(formatMenuConfig))
73
+ }
74
+ }
48
75
  }
49
76
 
50
77
  if (view.selectionStart < view.selectionEnd) {
51
- menu.add(FORMAT_MENU_GROUP_ID, MENU_COPY_MARKDOWN_ID, 101, "Copy as Markdown")
78
+ if (selectionMenuConfig.copyAsMarkdown) {
79
+ menu.add(FORMAT_MENU_GROUP_ID, MENU_COPY_MARKDOWN_ID, 101, selectionMenuConfig.copyAsMarkdownLabel)
80
+ }
52
81
 
53
82
  customItemTexts.forEachIndexed { index, text ->
54
83
  menu
@@ -74,7 +103,7 @@ class InputContextMenu(
74
103
 
75
104
  val formatIndex = itemId - MENU_FORMAT_ITEM_BASE
76
105
  if (formatIndex in FORMAT_ITEMS.indices) {
77
- applyFormat(FORMAT_ITEMS[formatIndex].second)
106
+ applyFormat(FORMAT_ITEMS[formatIndex].first)
78
107
  mode.finish()
79
108
  return true
80
109
  }
@@ -96,6 +125,16 @@ class InputContextMenu(
96
125
  }
97
126
  }
98
127
 
128
+ private fun isFormatItemVisible(styleType: StyleType): Boolean =
129
+ when (styleType) {
130
+ StyleType.BOLD -> formatMenuConfig.bold
131
+ StyleType.ITALIC -> formatMenuConfig.italic
132
+ StyleType.UNDERLINE -> formatMenuConfig.underline
133
+ StyleType.STRIKETHROUGH -> formatMenuConfig.strikethrough
134
+ StyleType.SPOILER -> formatMenuConfig.spoiler
135
+ StyleType.LINK -> formatMenuConfig.link
136
+ }
137
+
99
138
  private fun applyFormat(styleType: StyleType) {
100
139
  val start = view.selectionStart
101
140
  val end = view.selectionEnd
@@ -168,14 +207,16 @@ class InputContextMenu(
168
207
  private const val CUSTOM_MENU_GROUP_ID = 2000
169
208
  private const val MENU_CUSTOM_BASE = 2001
170
209
 
171
- private val FORMAT_ITEMS =
210
+ // Order here defines submenu order and the index used by
211
+ // `MENU_FORMAT_ITEM_BASE + index` when handling clicks.
212
+ private val FORMAT_ITEMS: List<Pair<StyleType, (FormatMenuConfig) -> String>> =
172
213
  listOf(
173
- "Bold" to StyleType.BOLD,
174
- "Italic" to StyleType.ITALIC,
175
- "Underline" to StyleType.UNDERLINE,
176
- "Strikethrough" to StyleType.STRIKETHROUGH,
177
- "Spoiler" to StyleType.SPOILER,
178
- "Link" to StyleType.LINK,
214
+ StyleType.BOLD to { it.boldLabel },
215
+ StyleType.ITALIC to { it.italicLabel },
216
+ StyleType.UNDERLINE to { it.underlineLabel },
217
+ StyleType.STRIKETHROUGH to { it.strikethroughLabel },
218
+ StyleType.SPOILER to { it.spoilerLabel },
219
+ StyleType.LINK to { it.linkLabel },
179
220
  )
180
221
  }
181
222
  }
@@ -41,3 +41,20 @@ data class MarkdownASTNode(
41
41
 
42
42
  fun getAttribute(key: String): String? = attributes[key]
43
43
  }
44
+
45
+ // A node type that md4c emits as a standalone block stacked vertically, as opposed to an inline span.
46
+ internal fun MarkdownASTNode.NodeType.isTopLevelBlock(): Boolean =
47
+ when (this) {
48
+ MarkdownASTNode.NodeType.Paragraph,
49
+ MarkdownASTNode.NodeType.Heading,
50
+ MarkdownASTNode.NodeType.Blockquote,
51
+ MarkdownASTNode.NodeType.UnorderedList,
52
+ MarkdownASTNode.NodeType.OrderedList,
53
+ MarkdownASTNode.NodeType.CodeBlock,
54
+ MarkdownASTNode.NodeType.ThematicBreak,
55
+ MarkdownASTNode.NodeType.Table,
56
+ MarkdownASTNode.NodeType.LatexMathDisplay,
57
+ -> true
58
+
59
+ else -> false
60
+ }
@@ -40,6 +40,7 @@ class HeadingRenderer(
40
40
  HeadingSpan(
41
41
  level,
42
42
  config.style,
43
+ factory.styleCache,
43
44
  ),
44
45
  start,
45
46
  end,
@@ -52,10 +52,10 @@ class SpanStyleCache(
52
52
  val paragraphColor = style.paragraphStyle.color
53
53
  return buildList {
54
54
  style.strongStyle.color
55
- ?.takeIf { it != 0 }
55
+ ?.takeIf { it != 0 && it != paragraphColor }
56
56
  ?.let { add(it) }
57
57
  style.emphasisStyle.color
58
- ?.takeIf { it != 0 }
58
+ ?.takeIf { it != 0 && it != paragraphColor }
59
59
  ?.let { add(it) }
60
60
  style.linkStyle.color
61
61
  .takeIf { it != 0 && it != paragraphColor }
@@ -68,10 +68,10 @@ class SpanStyleCache(
68
68
  style
69
69
  .codeStyle
70
70
  .color
71
- .takeIf { it != 0 }
71
+ .takeIf { it != 0 && it != paragraphColor }
72
72
  ?.let { add(it) }
73
73
  style.taskListStyle.checkedTextColor
74
- .takeIf { it != 0 }
74
+ .takeIf { it != 0 && it != paragraphColor }
75
75
  ?.let { add(it) }
76
76
  }.toIntArray()
77
77
  }
@@ -4,11 +4,14 @@ import android.annotation.SuppressLint
4
4
  import android.graphics.Typeface
5
5
  import android.text.TextPaint
6
6
  import android.text.style.MetricAffectingSpan
7
+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
7
8
  import com.swmansion.enriched.markdown.styles.StyleConfig
9
+ import com.swmansion.enriched.markdown.utils.text.extensions.applyColorPreserving
8
10
 
9
11
  class HeadingSpan(
10
12
  val level: Int,
11
13
  styleConfig: StyleConfig,
14
+ private val styleCache: SpanStyleCache,
12
15
  ) : MetricAffectingSpan() {
13
16
  private val fontSize: Float = styleConfig.headingStyles[level]!!.fontSize
14
17
  private val color: Int = styleConfig.headingStyles[level]!!.color
@@ -16,7 +19,11 @@ class HeadingSpan(
16
19
 
17
20
  override fun updateDrawState(tp: TextPaint) {
18
21
  applyHeadingStyle(tp)
19
- tp.color = color
22
+ // Inline spans (link, code, strong/em with their own color) are attached to the range
23
+ // BEFORE this block span, and TextLine replays every CharacterStyle in insertion order,
24
+ // so an unconditional `tp.color = color` here would repaint them. Same guard used by
25
+ // BlockquoteSpan, BaseListSpan, CodeBlockSpan, StrongSpan and EmphasisSpan.
26
+ tp.applyColorPreserving(color, *styleCache.colorsToPreserve)
20
27
  }
21
28
 
22
29
  override fun updateMeasureState(tp: TextPaint) {
@@ -1,6 +1,8 @@
1
1
  package com.swmansion.enriched.markdown.spans
2
2
 
3
3
  import android.graphics.Paint
4
+ import android.text.Spanned
5
+ import android.text.style.ReplacementSpan
4
6
  import kotlin.math.ceil
5
7
  import kotlin.math.floor
6
8
  import android.text.style.LineHeightSpan as AndroidLineHeightSpan
@@ -18,10 +20,34 @@ class LineHeightSpan(
18
20
  v: Int,
19
21
  fm: Paint.FontMetricsInt?,
20
22
  ) {
21
- if (fm == null) return
23
+ if (fm == null || lineHeight <= 0) return
22
24
 
23
25
  val leading = lineHeight - ((-fm.ascent) + fm.descent)
26
+
27
+ // A ReplacementSpan on this line — inline math, or the placeholder MeasurementStore
28
+ // swaps in to measure it off the main thread — reports its own metrics from getSize().
29
+ // Shrinking those back down to the configured line height cuts the line box below what
30
+ // the span actually draws, and can drive descent negative so the next line starts above
31
+ // this one's baseline. Those lines are expanded only, never compressed; every other line
32
+ // keeps the exact clamp it had before. The lookup only runs on the rare negative-leading
33
+ // path, so the common case stays allocation-free.
34
+ if (leading <= 0 && hasSelfMeasuringSpan(text, start, end)) return
35
+
24
36
  fm.ascent -= ceil(leading / 2.0f).toInt()
25
37
  fm.descent += floor(leading / 2.0f).toInt()
26
38
  }
39
+
40
+ /**
41
+ * Images are excluded on purpose. Block images already keep this span off their range via
42
+ * `applyLineHeightSkippingImages`, and inline ones never touch the metrics at all, so
43
+ * letting them through here would change image layout instead of math layout.
44
+ */
45
+ private fun hasSelfMeasuringSpan(
46
+ text: CharSequence?,
47
+ start: Int,
48
+ end: Int,
49
+ ): Boolean {
50
+ val spanned = text as? Spanned ?: return false
51
+ return spanned.getSpans(start, end, ReplacementSpan::class.java).any { it !is ImageSpan }
52
+ }
27
53
  }
@@ -4,6 +4,8 @@ import com.facebook.react.bridge.ReadableMap
4
4
 
5
5
  data class InlineMathStyle(
6
6
  val color: Int,
7
+ val paddingVertical: Float,
8
+ val fontWeight: String,
7
9
  ) {
8
10
  companion object {
9
11
  fun fromReadableMap(
@@ -11,7 +13,9 @@ data class InlineMathStyle(
11
13
  parser: StyleParser,
12
14
  ): InlineMathStyle {
13
15
  val color = parser.parseColor(map, "color")
14
- return InlineMathStyle(color)
16
+ val paddingVertical = parser.toPixelFromDIP(map.getDouble("paddingVertical").toFloat())
17
+ val fontWeight = parser.parseString(map, "fontWeight", "normal")
18
+ return InlineMathStyle(color, paddingVertical, fontWeight)
15
19
  }
16
20
  }
17
21
  }
@@ -0,0 +1,25 @@
1
+ package com.swmansion.enriched.markdown.styles
2
+
3
+ /**
4
+ * Weight a formula can actually be typeset at.
5
+ *
6
+ * The style model keeps `fontWeight` as a [String], the way every other
7
+ * `fontWeight` prop in the codebase does, so a value like `"600"` survives as
8
+ * far as the renderer. This enum is the narrowing step at that last hop — see
9
+ * `ratexTypefaceLoader`, the only caller — so adding a third weight is a
10
+ * compile error at the draw site rather than a value that silently falls back
11
+ * to regular. Mirrors ENRMMathFontWeight.h on iOS.
12
+ */
13
+ enum class MathFontWeight {
14
+ NORMAL,
15
+ BOLD,
16
+ ;
17
+
18
+ companion object {
19
+ fun fromString(value: String): MathFontWeight =
20
+ when (value.lowercase()) {
21
+ "bold" -> BOLD
22
+ else -> NORMAL
23
+ }
24
+ }
25
+ }
@@ -10,6 +10,7 @@ data class MathStyle(
10
10
  val marginTop: Float,
11
11
  val marginBottom: Float,
12
12
  val textAlign: String,
13
+ val fontWeight: String,
13
14
  ) {
14
15
  companion object {
15
16
  fun fromReadableMap(
@@ -23,7 +24,17 @@ data class MathStyle(
23
24
  val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
24
25
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
25
26
  val textAlign = parser.parseString(map, "textAlign", "center")
26
- return MathStyle(fontSize, color, backgroundColor, padding, marginTop, marginBottom, textAlign)
27
+ val fontWeight = parser.parseString(map, "fontWeight", "normal")
28
+ return MathStyle(
29
+ fontSize,
30
+ color,
31
+ backgroundColor,
32
+ padding,
33
+ marginTop,
34
+ marginBottom,
35
+ textAlign,
36
+ fontWeight,
37
+ )
27
38
  }
28
39
  }
29
40
  }
@@ -4,6 +4,7 @@ import android.view.View
4
4
  import com.facebook.react.bridge.ReadableArray
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
  import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
7
8
  import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
8
9
  import com.swmansion.enriched.markdown.events.LinkLongPressEvent
9
10
  import com.swmansion.enriched.markdown.events.LinkPressEvent
@@ -81,11 +82,11 @@ fun emitContextMenuItemPress(
81
82
 
82
83
  fun parseMd4cFlags(flags: ReadableMap?): Md4cFlags =
83
84
  Md4cFlags(
84
- underline = flags?.getBoolean("underline") ?: false,
85
- latexMath = FeatureFlags.IS_MATH_ENABLED && (flags?.getBoolean("latexMath") ?: true),
86
- superscript = flags?.getBoolean("superscript") ?: false,
87
- subscript = flags?.getBoolean("subscript") ?: false,
88
- highlight = flags?.getBoolean("highlight") ?: false,
85
+ underline = flags.getBooleanOrDefault("underline", false),
86
+ latexMath = FeatureFlags.IS_MATH_ENABLED && flags.getBooleanOrDefault("latexMath", true),
87
+ superscript = flags.getBooleanOrDefault("superscript", false),
88
+ subscript = flags.getBooleanOrDefault("subscript", false),
89
+ highlight = flags.getBooleanOrDefault("highlight", false),
89
90
  )
90
91
 
91
92
  fun parseContextMenuItems(value: ReadableArray?): List<String> =
@@ -94,7 +95,36 @@ fun parseContextMenuItems(value: ReadableArray?): List<String> =
94
95
  fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
95
96
  if (value == null) return SelectionMenuConfig()
96
97
  return SelectionMenuConfig(
97
- copyAsMarkdown = value.getBoolean("copyAsMarkdown"),
98
- copyImageUrl = value.getBoolean("copyImageUrl"),
98
+ copyAsMarkdown = value.getBooleanOrDefault("copyAsMarkdown", true),
99
+ copyImageUrl = value.getBooleanOrDefault("copyImageUrl", true),
100
+ copyLabel = value.getString("copyLabel") ?: "",
101
+ copyAsMarkdownLabel = value.getString("copyAsMarkdownLabel") ?: "",
102
+ copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
103
+ copyImageUrlsLabel = value.getString("copyImageUrlsLabel") ?: "",
104
+ copyImageUrlPluralTemplates = parseStringList(value.getArray("copyImageUrlPluralTemplates")),
99
105
  )
100
106
  }
107
+
108
+ fun parseAccessibilityLabels(value: ReadableMap?): AccessibilityLabels {
109
+ if (value == null) return AccessibilityLabels()
110
+ val list = value.getMap("list")
111
+ val blockquote = value.getMap("blockquote")
112
+ val table = value.getMap("table")
113
+ val math = value.getMap("math")
114
+ val rotor = value.getMap("rotor")
115
+ return AccessibilityLabels(
116
+ bulletPoint = list?.getString("bulletPoint") ?: "",
117
+ nestedBulletPoint = list?.getString("nestedBulletPoint") ?: "",
118
+ orderedItem = list?.getString("orderedItem") ?: "",
119
+ nestedOrderedItem = list?.getString("nestedOrderedItem") ?: "",
120
+ blockquote = blockquote?.getString("quote") ?: "",
121
+ nestedBlockquote = blockquote?.getString("nestedQuote") ?: "",
122
+ tableRow = table?.getString("row") ?: "",
123
+ mathEquation = math?.getString("equation") ?: "",
124
+ rotorHeadings = rotor?.getString("headings") ?: "",
125
+ rotorLinks = rotor?.getString("links") ?: "",
126
+ rotorImages = rotor?.getString("images") ?: "",
127
+ )
128
+ }
129
+
130
+ private fun parseStringList(value: ReadableArray?): List<String> = (0 until (value?.size() ?: 0)).mapNotNull { value?.getString(it) }
@@ -76,6 +76,8 @@ object MarkdownStyleParser {
76
76
  if (mathMap?.hasKey("selectedBackgroundColor") == true) mathMap.getInt("selectedBackgroundColor") else null
77
77
  val selectedBorderColor =
78
78
  if (mathMap?.hasKey("selectedBorderColor") == true) mathMap.getInt("selectedBorderColor") else null
79
+ val fontWeight =
80
+ (if (mathMap?.hasKey("fontWeight") == true) mathMap.getString("fontWeight") else null) ?: "normal"
79
81
 
80
82
  return MathChipStyle(
81
83
  color = color,
@@ -88,6 +90,7 @@ object MarkdownStyleParser {
88
90
  paddingVertical = paddingVertical,
89
91
  selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
90
92
  selectedBorderColor = nullIfTransparent(selectedBorderColor),
93
+ fontWeight = fontWeight,
91
94
  )
92
95
  }
93
96
 
@@ -143,6 +146,12 @@ object MarkdownStyleParser {
143
146
  } else {
144
147
  inlineFallback.selectedBorderColor
145
148
  }
149
+ val fontWeight =
150
+ if (mathBlockMap?.hasKey("fontWeight") == true) {
151
+ mathBlockMap.getString("fontWeight") ?: "normal"
152
+ } else {
153
+ inlineFallback.fontWeight
154
+ }
146
155
  val marginTop =
147
156
  PixelUtil.toPixelFromDIP(
148
157
  if (mathBlockMap?.hasKey("marginTop") == true) mathBlockMap.getDouble("marginTop").toFloat() else 8f,
@@ -170,6 +179,7 @@ object MarkdownStyleParser {
170
179
  paddingVertical = paddingVertical,
171
180
  selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
172
181
  selectedBorderColor = nullIfTransparent(selectedBorderColor),
182
+ fontWeight = fontWeight,
173
183
  marginTop = marginTop,
174
184
  marginBottom = marginBottom,
175
185
  alignment = alignment,
@@ -14,8 +14,16 @@ fun SpannableStringBuilder.isInlineImage(): Boolean {
14
14
  return lastChar != '\n' && lastChar != '\u200B'
15
15
  }
16
16
 
17
- /** Swaps MathInlineSpans for MathInlinePlaceholderSpans safe for background-thread measurement. */
18
- fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
17
+ /**
18
+ * Swaps MathInlineSpans for MathInlinePlaceholderSpans safe for background-thread measurement.
19
+ *
20
+ * [paddingVertical] must be the same `inlineMath.paddingVertical` (in px) the real spans were
21
+ * built with, or the measured line box won't match the drawn one.
22
+ */
23
+ fun SpannableString.replaceMathSpansWithPlaceholders(
24
+ context: Context,
25
+ paddingVertical: Float,
26
+ ) {
19
27
  if (!FeatureFlags.IS_MATH_ENABLED) return
20
28
 
21
29
  try {
@@ -42,7 +50,7 @@ fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
42
50
  @Suppress("UNCHECKED_CAST")
43
51
  val results = measureMethod.invoke(null, context, requests) as? List<MathMetrics> ?: return
44
52
 
45
- val placeholderCtor = placeholderClass.getConstructor(MathMetrics::class.java)
53
+ val placeholderCtor = placeholderClass.getConstructor(MathMetrics::class.java, Float::class.javaPrimitiveType)
46
54
 
47
55
  mathSpans.forEachIndexed { i, oldSpan ->
48
56
  val metrics = results.getOrNull(i) ?: return@forEachIndexed
@@ -53,7 +61,7 @@ fun SpannableString.replaceMathSpansWithPlaceholders(context: Context) {
53
61
 
54
62
  if (start != -1 && end != -1) {
55
63
  removeSpan(oldSpan)
56
- val newSpan = placeholderCtor.newInstance(metrics)
64
+ val newSpan = placeholderCtor.newInstance(metrics, paddingVertical)
57
65
  setSpan(newSpan, start, end, flags)
58
66
  }
59
67
  }
@@ -26,18 +26,40 @@ fun View.emitLinkLongPressEvent(url: String) {
26
26
  /**
27
27
  * Cancels the JS touch for an active link tap, preventing parent
28
28
  * Pressable/TouchableOpacity from firing onPress for the same tap.
29
+ *
30
+ * Also requests that parent ViewGroups do not intercept subsequent touch
31
+ * events in this gesture. This is necessary for react-native-gesture-handler's
32
+ * Pressable, which intercepts at the native ViewGroup level and would otherwise
33
+ * steal the gesture before ACTION_UP reaches the TextView.
29
34
  */
30
35
  fun TextView.cancelJSTouchForLinkTap(event: MotionEvent) {
31
36
  val currentMovementMethod = movementMethod
32
37
  if (currentMovementMethod is LinkLongPressMovementMethod && currentMovementMethod.isLinkTouchActive) {
38
+ parent?.requestDisallowInterceptTouchEvent(true)
33
39
  NativeGestureUtil.notifyNativeGestureStarted(this, event)
34
40
  }
35
41
  }
36
42
 
43
+ /**
44
+ * Re-allows parent ViewGroups to intercept touch events once the link
45
+ * touch is no longer active (slop exceeded, gesture ended, or cancelled).
46
+ * This restores normal scrolling behavior for parent ScrollView/RecyclerView.
47
+ *
48
+ * Must be called after [LinkLongPressMovementMethod.onTouchEvent] has
49
+ * processed the event, since that is where [isLinkTouchActive] is updated.
50
+ */
51
+ fun TextView.reallowParentInterceptIfLinkReleased() {
52
+ val currentMovementMethod = movementMethod
53
+ if (currentMovementMethod is LinkLongPressMovementMethod && !currentMovementMethod.isLinkTouchActive) {
54
+ parent?.requestDisallowInterceptTouchEvent(false)
55
+ }
56
+ }
57
+
37
58
  /**
38
59
  * Cancels the JS touch unconditionally, preventing parent
39
60
  * Pressable/TouchableOpacity from firing onPress for the same gesture.
40
61
  */
41
62
  fun View.cancelJSTouchForCheckboxTap(event: MotionEvent) {
63
+ parent?.requestDisallowInterceptTouchEvent(true)
42
64
  NativeGestureUtil.notifyNativeGestureStarted(this, event)
43
65
  }
@@ -25,8 +25,30 @@ private const val MENU_ITEM_CUSTOM_GROUP = 2001
25
25
  data class SelectionMenuConfig(
26
26
  val copyAsMarkdown: Boolean = true,
27
27
  val copyImageUrl: Boolean = true,
28
+ val copyLabel: String = "",
29
+ val copyAsMarkdownLabel: String = "",
30
+ val copyImageUrlLabel: String = "",
31
+ val copyImageUrlsLabel: String = "",
32
+ // Plural templates indexed by image count (0..100). Empty when no pluralLabels
33
+ // are set; counts > 100 use copyImageUrlsLabel (the "other" form).
34
+ val copyImageUrlPluralTemplates: List<String> = emptyList(),
28
35
  )
29
36
 
37
+ /**
38
+ * Resolves the "Copy Image URL(s)" menu title for the given image count. Uses the
39
+ * precomputed plural templates when present (counts > 100 use the "other" form),
40
+ * otherwise the singular/`{count}` templates. All labels are resolved JS-side.
41
+ */
42
+ private fun SelectionMenuConfig.imageUrlsTitle(count: Int): String {
43
+ val template =
44
+ when {
45
+ copyImageUrlPluralTemplates.isNotEmpty() && count in 0..100 -> copyImageUrlPluralTemplates[count]
46
+ count == 1 -> copyImageUrlLabel
47
+ else -> copyImageUrlsLabel
48
+ }
49
+ return template.replace("{count}", count.toString())
50
+ }
51
+
30
52
  /**
31
53
  * Creates an ActionMode.Callback that adds custom copy options and
32
54
  * overrides the default "Copy" action to include HTML for rich text support.
@@ -56,12 +78,24 @@ fun createSelectionActionModeCallback(
56
78
 
57
79
  val selectionMenuConfig = getSelectionMenuConfig()
58
80
 
81
+ // The system Copy item is added by the platform; relabel it so it
82
+ // matches the rest of the localized menu (iOS rebuilds the menu so
83
+ // gets this for free; Android only intercepts the click).
84
+ if (selectionMenuConfig.copyLabel.isNotEmpty()) {
85
+ menu.findItem(android.R.id.copy)?.title = selectionMenuConfig.copyLabel
86
+ }
87
+
59
88
  if (
60
89
  selectionMenuConfig.copyAsMarkdown &&
61
90
  textView.selectionStart >= 0 &&
62
91
  textView.selectionEnd > textView.selectionStart
63
92
  ) {
64
- menu.add(Menu.NONE, MENU_ITEM_COPY_MARKDOWN, Menu.NONE, "Copy as Markdown")
93
+ menu.add(
94
+ Menu.NONE,
95
+ MENU_ITEM_COPY_MARKDOWN,
96
+ Menu.NONE,
97
+ selectionMenuConfig.copyAsMarkdownLabel,
98
+ )
65
99
  }
66
100
 
67
101
  if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
@@ -80,12 +114,7 @@ fun createSelectionActionModeCallback(
80
114
  emptyList()
81
115
  }
82
116
  if (imageUrls.isNotEmpty()) {
83
- val title =
84
- if (imageUrls.size == 1) {
85
- "Copy Image URL"
86
- } else {
87
- "Copy ${imageUrls.size} Image URLs"
88
- }
117
+ val title = selectionMenuConfig.imageUrlsTitle(imageUrls.size)
89
118
  menu.add(Menu.NONE, MENU_ITEM_COPY_IMAGE_URL, Menu.NONE, title)
90
119
  }
91
120