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

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 (150) hide show
  1. package/ReactNativeEnrichedMarkdown.podspec +62 -11
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +9 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +3 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  7. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +744 -0
  10. package/android/src/main/baseline-prof.txt +75 -2
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +10 -2
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +10 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +9 -2
  15. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +10 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/AccessibilityLabels.kt +15 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +44 -7
  18. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +52 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +89 -11
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +19 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +22 -3
  22. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputRemend.kt +1 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -1
  24. package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +54 -13
  25. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +17 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +4 -4
  27. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +30 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +22 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +36 -7
  30. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +73 -3
  31. package/android/src/main/jni/CMakeLists.txt +15 -1
  32. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +24 -2
  33. package/app.plugin.js +2 -1
  34. package/cpp/parser/MD4CParser.cpp +1 -1
  35. package/cpp/wasm/build.sh +9 -9
  36. package/docs/ACCESSIBILITY.md +172 -0
  37. package/docs/API_REFERENCE.md +948 -0
  38. package/docs/COPY_OPTIONS.md +127 -0
  39. package/docs/ELEMENTS_STRUCTURE.md +112 -0
  40. package/docs/IMAGE_CACHING.md +25 -0
  41. package/docs/INPUT.md +233 -0
  42. package/docs/LATEX_MATH.md +136 -0
  43. package/docs/MACOS.md +19 -0
  44. package/docs/MARKDOWN_STREAMING.md +36 -0
  45. package/docs/MENTIONS.md +97 -0
  46. package/docs/RTL.md +82 -0
  47. package/docs/STYLES.md +519 -0
  48. package/docs/TEXT.md +110 -0
  49. package/docs/WEB.md +49 -0
  50. package/ios/EnrichedMarkdown.mm +82 -25
  51. package/ios/EnrichedMarkdownText.mm +37 -5
  52. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  53. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +744 -0
  54. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +15 -0
  55. package/ios/input/ENRMFormattingStore.h +5 -0
  56. package/ios/input/ENRMFormattingStore.mm +34 -0
  57. package/ios/input/ENRMInputTextView.h +2 -0
  58. package/ios/input/ENRMInputTextView.mm +8 -17
  59. package/ios/input/ENRMMarkdownSerializer.mm +1 -1
  60. package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +71 -53
  61. package/ios/input/EnrichedMarkdownTextInput+Internal.h +26 -0
  62. package/ios/input/EnrichedMarkdownTextInput.h +1 -0
  63. package/ios/input/EnrichedMarkdownTextInput.mm +230 -29
  64. package/ios/input/styles/ENRMBoldStyleHandler.mm +3 -2
  65. package/ios/input/styles/ENRMItalicStyleHandler.mm +3 -2
  66. package/ios/renderer/AttributedRenderer.m +51 -6
  67. package/ios/renderer/ENRMMathInlineRenderer.m +5 -1
  68. package/ios/styles/ENRMFontSlot.h +1 -1
  69. package/ios/utils/ENRMAccessibilityLabels.h +36 -0
  70. package/ios/utils/ENRMAccessibilityLabels.m +5 -0
  71. package/ios/utils/EditMenuUtils+macOS.m +4 -5
  72. package/ios/utils/EditMenuUtils.h +70 -0
  73. package/ios/utils/EditMenuUtils.m +14 -11
  74. package/ios/utils/LastElementUtils.h +39 -2
  75. package/ios/utils/MarkdownAccessibilityElementBuilder.h +18 -16
  76. package/ios/utils/MarkdownAccessibilityElementBuilder.m +64 -32
  77. package/ios/utils/ParagraphStyleUtils.m +21 -2
  78. package/ios/views/ENRMMathContainerView.h +8 -0
  79. package/ios/views/ENRMMathContainerView.m +7 -5
  80. package/ios/views/ENRMTableIOSGridView.h +40 -0
  81. package/ios/views/ENRMTableIOSGridView.m +216 -0
  82. package/ios/views/EnrichedMarkdownInternalText.h +2 -0
  83. package/ios/views/EnrichedMarkdownInternalText.m +2 -0
  84. package/ios/views/TableContainerView.h +8 -0
  85. package/ios/views/TableContainerView.m +72 -128
  86. package/lib/module/EnrichedMarkdownNativeComponent.ts +37 -0
  87. package/lib/module/EnrichedMarkdownTextInput.js +48 -1
  88. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  89. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +35 -0
  90. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +37 -0
  91. package/lib/module/accessibilityLabelDefaults.js +64 -0
  92. package/lib/module/accessibilityLabelDefaults.js.map +1 -0
  93. package/lib/module/index.js +1 -0
  94. package/lib/module/index.js.map +1 -1
  95. package/lib/module/native/EnrichedMarkdownText.js +72 -4
  96. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  97. package/lib/module/normalizeMenuItem.js +38 -0
  98. package/lib/module/normalizeMenuItem.js.map +1 -0
  99. package/lib/module/types/AccessibilityLabels.js +2 -0
  100. package/lib/module/types/AccessibilityLabels.js.map +1 -0
  101. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +34 -0
  102. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  103. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +55 -1
  104. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  105. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +30 -0
  106. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  107. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +34 -0
  108. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  109. package/lib/typescript/src/accessibilityLabelDefaults.d.ts +16 -0
  110. package/lib/typescript/src/accessibilityLabelDefaults.d.ts.map +1 -0
  111. package/lib/typescript/src/index.d.ts +4 -2
  112. package/lib/typescript/src/index.d.ts.map +1 -1
  113. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
  114. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  115. package/lib/typescript/src/normalizeMenuItem.d.ts +7 -0
  116. package/lib/typescript/src/normalizeMenuItem.d.ts.map +1 -0
  117. package/lib/typescript/src/types/AccessibilityLabels.d.ts +114 -0
  118. package/lib/typescript/src/types/AccessibilityLabels.d.ts.map +1 -0
  119. package/lib/typescript/src/types/MarkdownTextProps.d.ts +78 -9
  120. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  121. package/package.json +4 -2
  122. package/plugin/build/withAndroidMath.js +19 -0
  123. package/plugin/build/withIosMath.js +28 -0
  124. package/plugin/build/withReactNativeEnrichedMarkdown.js +11 -0
  125. package/plugin/tsconfig.build.json +16 -0
  126. package/src/EnrichedMarkdownNativeComponent.ts +37 -0
  127. package/src/EnrichedMarkdownTextInput.tsx +117 -1
  128. package/src/EnrichedMarkdownTextInputNativeComponent.ts +35 -0
  129. package/src/EnrichedMarkdownTextNativeComponent.ts +37 -0
  130. package/src/accessibilityLabelDefaults.ts +83 -0
  131. package/src/index.tsx +11 -0
  132. package/src/native/EnrichedMarkdownText.tsx +120 -6
  133. package/src/normalizeMenuItem.ts +52 -0
  134. package/src/types/AccessibilityLabels.ts +118 -0
  135. package/src/types/MarkdownTextProps.ts +74 -9
  136. package/lib/module/plugin/withAndroidMath.js +0 -20
  137. package/lib/module/plugin/withAndroidMath.js.map +0 -1
  138. package/lib/module/plugin/withIosMath.js +0 -23
  139. package/lib/module/plugin/withIosMath.js.map +0 -1
  140. package/lib/module/plugin/withReactNativeEnrichedMarkdown.js +0 -16
  141. package/lib/module/plugin/withReactNativeEnrichedMarkdown.js.map +0 -1
  142. package/lib/typescript/src/plugin/withAndroidMath.d.ts +0 -5
  143. package/lib/typescript/src/plugin/withAndroidMath.d.ts.map +0 -1
  144. package/lib/typescript/src/plugin/withIosMath.d.ts +0 -5
  145. package/lib/typescript/src/plugin/withIosMath.d.ts.map +0 -1
  146. package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts +0 -6
  147. package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts.map +0 -1
  148. /package/{src/plugin → plugin/src}/withAndroidMath.ts +0 -0
  149. /package/{src/plugin → plugin/src}/withIosMath.ts +0 -0
  150. /package/{src/plugin → plugin/src}/withReactNativeEnrichedMarkdown.ts +0 -0
@@ -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
 
@@ -15,9 +15,12 @@ import android.text.TextPaint
15
15
  import android.text.style.AlignmentSpan
16
16
  import android.text.style.MetricAffectingSpan
17
17
  import android.view.Gravity
18
+ import android.view.MotionEvent
18
19
  import android.view.View
19
20
  import android.widget.FrameLayout
20
21
  import android.widget.HorizontalScrollView
22
+ import androidx.core.view.ViewCompat
23
+ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
21
24
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode
22
25
  import com.swmansion.enriched.markdown.parser.MarkdownASTNode.NodeType
23
26
  import com.swmansion.enriched.markdown.renderer.Renderer
@@ -28,6 +31,8 @@ import com.swmansion.enriched.markdown.utils.common.serialization.MarkdownASTSer
28
31
  import com.swmansion.enriched.markdown.utils.text.conversion.HTMLGenerator
29
32
  import com.swmansion.enriched.markdown.utils.text.extensions.replaceMathSpansWithPlaceholders
30
33
  import com.swmansion.enriched.markdown.utils.text.view.LinkLongPressMovementMethod
34
+ import com.swmansion.enriched.markdown.utils.text.view.cancelJSTouchForLinkTap
35
+ import com.swmansion.enriched.markdown.utils.text.view.reallowParentInterceptIfLinkReleased
31
36
  import com.swmansion.enriched.markdown.views.ContextMenuPopup
32
37
  import kotlin.math.ceil
33
38
  import kotlin.math.max
@@ -49,12 +54,26 @@ class TableContainerView(
49
54
  var maxFontSizeMultiplier = 0f
50
55
  var onLinkPress: ((String) -> Unit)? = null
51
56
  var onLinkLongPress: ((String) -> Unit)? = null
57
+ var accessibilityLabels: AccessibilityLabels = AccessibilityLabels()
58
+ set(value) {
59
+ if (field == value) return
60
+ field = value
61
+ if (rowCount > 0) renderGrid()
62
+ }
63
+
64
+ var copyLabel: String = ""
65
+ var copyAsMarkdownLabel: String = ""
52
66
 
53
67
  private val scrollView =
54
68
  HorizontalScrollView(context).apply {
55
69
  isHorizontalScrollBarEnabled = true
56
70
  overScrollMode = View.OVER_SCROLL_NEVER
57
- addView(GridContainerView(context))
71
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
72
+ addView(
73
+ GridContainerView(context).apply {
74
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
75
+ },
76
+ )
58
77
  }
59
78
  private val gridContainer get() = scrollView.getChildAt(0) as GridContainerView
60
79
 
@@ -93,6 +112,7 @@ class TableContainerView(
93
112
  private var tableMarkdown = ""
94
113
 
95
114
  init {
115
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
96
116
  addView(scrollView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
97
117
  }
98
118
 
@@ -191,6 +211,7 @@ class TableContainerView(
191
211
  showContextMenu(view)
192
212
  true
193
213
  }
214
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
194
215
  }
195
216
 
196
217
  gridContainer.addView(
@@ -204,12 +225,51 @@ class TableContainerView(
204
225
  if (col < row.size) addTextToCell(cellBg, row[col], columnWidth, rowHeight)
205
226
  if (!isRtl) xOffset += columnWidth
206
227
  }
228
+
229
+ addRowAccessibilityOverlay(row, rowIndex, isHeaderRow, yOffset, rowHeight)
230
+
207
231
  if (!isHeaderRow) bodyRowIndex++
208
232
  yOffset += rowHeight
209
233
  }
210
234
  gridContainer.layoutParams = LayoutParams(ceil(totalTableWidth).toInt(), ceil(totalTableHeight).toInt())
211
235
  }
212
236
 
237
+ private fun addRowAccessibilityOverlay(
238
+ row: List<TableCellData>,
239
+ rowIndex: Int,
240
+ isHeaderRow: Boolean,
241
+ yOffset: Float,
242
+ rowHeight: Float,
243
+ ) {
244
+ val joinedContent = row.joinToString(", ") { it.plainText }
245
+ val description =
246
+ accessibilityLabels.tableRow
247
+ .replace("{n}", (rowIndex + 1).toString())
248
+ .replace("{content}", joinedContent)
249
+
250
+ val overlay =
251
+ View(context).apply {
252
+ isClickable = false
253
+ isLongClickable = false
254
+ isFocusable = true
255
+ isScreenReaderFocusable = true
256
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
257
+ contentDescription = description
258
+ if (isHeaderRow) ViewCompat.setAccessibilityHeading(this, true)
259
+ }
260
+ gridContainer.addView(
261
+ overlay,
262
+ 0,
263
+ LayoutParams(
264
+ ceil(totalTableWidth).toInt(),
265
+ ceil(rowHeight + tableStyle.borderWidth).toInt(),
266
+ ).apply {
267
+ leftMargin = 0
268
+ topMargin = ceil(yOffset).toInt()
269
+ },
270
+ )
271
+ }
272
+
213
273
  private fun addTextToCell(
214
274
  container: CellBackgroundView,
215
275
  data: TableCellData,
@@ -279,7 +339,7 @@ class TableContainerView(
279
339
  private fun showContextMenu(anchor: View) {
280
340
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
281
341
  ContextMenuPopup.show(anchor, this) {
282
- item(ContextMenuPopup.Icon.COPY, "Copy") {
342
+ item(ContextMenuPopup.Icon.COPY, copyLabel) {
283
343
  val plainText = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
284
344
  if (plainText.isNotEmpty()) {
285
345
  val displayMetrics = context.resources.displayMetrics
@@ -291,7 +351,7 @@ class TableContainerView(
291
351
  clipboard.setPrimaryClip(ClipData.newHtmlText("Table", plainText, html))
292
352
  }
293
353
  }
294
- item(ContextMenuPopup.Icon.DOCUMENT, "Copy as Markdown") {
354
+ item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
295
355
  if (tableMarkdown.isNotEmpty()) clipboard.setPrimaryClip(ClipData.newPlainText("Table", tableMarkdown))
296
356
  }
297
357
  }
@@ -488,6 +548,16 @@ class TableContainerView(
488
548
  movementMethod = LinkLongPressMovementMethod.createInstance()
489
549
  layoutDirection = View.LAYOUT_DIRECTION_LOCALE
490
550
  textDirection = View.TEXT_DIRECTION_LOCALE
551
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
552
+ }
553
+
554
+ override fun onTouchEvent(event: MotionEvent): Boolean {
555
+ val result = super.onTouchEvent(event)
556
+ when (event.action) {
557
+ MotionEvent.ACTION_DOWN -> cancelJSTouchForLinkTap(event)
558
+ else -> reallowParentInterceptIfLinkReleased()
559
+ }
560
+ return result
491
561
  }
492
562
  }
493
563
 
@@ -8,7 +8,21 @@ set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
8
8
  set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/generated/jni)
9
9
  set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})
10
10
 
11
- set(CPP_ROOT "${LIB_ANDROID_DIR}/../cpp")
11
+ # cpp/ is a symlink to packages/core/cpp in the monorepo; published tarballs
12
+ # ship real files (materialized at prepack). Use paths relative to this file —
13
+ # Android's cross-compile CMake pass cannot access absolute paths outside the
14
+ # module via symlinks or REALPATH.
15
+ set(CPP_IN_PACKAGE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp")
16
+ set(CPP_IN_MONOREPO "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../core/cpp")
17
+
18
+ if(EXISTS "${CPP_IN_PACKAGE}/md4c/md4c.c")
19
+ set(CPP_ROOT "${CPP_IN_PACKAGE}")
20
+ elseif(EXISTS "${CPP_IN_MONOREPO}/md4c/md4c.c")
21
+ set(CPP_ROOT "${CPP_IN_MONOREPO}")
22
+ else()
23
+ message(FATAL_ERROR "EnrichedMarkdown C++ sources not found. Expected cpp/ or packages/core/cpp.")
24
+ endif()
25
+
12
26
  set(ANDROID_CPP_DIR "${LIB_ANDROID_DIR}/src/main/cpp")
13
27
 
14
28
  file(GLOB MD4C_SOURCES "${CPP_ROOT}/md4c/*.c")
@@ -11,6 +11,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
11
11
  import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
12
12
  import android.widget.FrameLayout
13
13
  import android.widget.HorizontalScrollView
14
+ import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
14
15
  import com.swmansion.enriched.markdown.spans.MathMeasureHelper
15
16
  import com.swmansion.enriched.markdown.spans.MathMeasureRequest
16
17
  import com.swmansion.enriched.markdown.spans.MathRenderMode
@@ -30,6 +31,16 @@ class MathContainerView(
30
31
  private val scrollView = HorizontalScrollView(context)
31
32
  private var cachedLatex: String = ""
32
33
 
34
+ var accessibilityLabels: AccessibilityLabels = AccessibilityLabels()
35
+ set(value) {
36
+ field = value
37
+ updateAccessibilityLabel()
38
+ }
39
+
40
+ // Set reflectively by EnrichedMarkdown (math is an optional module).
41
+ var copyLabel: String = ""
42
+ var copyAsMarkdownLabel: String = ""
43
+
33
44
  override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
34
45
  override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()
35
46
 
@@ -64,11 +75,17 @@ class MathContainerView(
64
75
  isHorizontalScrollBarEnabled = true
65
76
  overScrollMode = View.OVER_SCROLL_NEVER
66
77
  isFillViewport = true
78
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
67
79
  addView(mathWrapper, LayoutParams(WRAP_CONTENT, WRAP_CONTENT))
68
80
  }
69
81
 
70
82
  addView(scrollView, LayoutParams(MATCH_PARENT, WRAP_CONTENT))
71
83
 
84
+ isFocusable = true
85
+ isScreenReaderFocusable = true
86
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
87
+ updateAccessibilityLabel()
88
+
72
89
  setOnLongClickListener { view ->
73
90
  showContextMenu(view)
74
91
  true
@@ -90,15 +107,20 @@ class MathContainerView(
90
107
  }
91
108
  mathView.requestLayout()
92
109
  mathView.invalidate()
110
+ updateAccessibilityLabel()
111
+ }
112
+
113
+ private fun updateAccessibilityLabel() {
114
+ contentDescription = accessibilityLabels.mathEquation.replace("{latex}", cachedLatex)
93
115
  }
94
116
 
95
117
  private fun showContextMenu(anchor: View) {
96
118
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
97
119
  ContextMenuPopup.show(anchor, this) {
98
- item(ContextMenuPopup.Icon.COPY, "Copy") {
120
+ item(ContextMenuPopup.Icon.COPY, copyLabel) {
99
121
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
100
122
  }
101
- item(ContextMenuPopup.Icon.DOCUMENT, "Copy as Markdown") {
123
+ item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel) {
102
124
  clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
103
125
  }
104
126
  }
package/app.plugin.js CHANGED
@@ -1 +1,2 @@
1
- module.exports = require('./lib/module/plugin/withReactNativeEnrichedMarkdown');
1
+ module.exports =
2
+ require('./plugin/build/withReactNativeEnrichedMarkdown').default;
@@ -536,4 +536,4 @@ const std::string MD4CParser::Impl::ATTR_LANGUAGE = "language";
536
536
  const std::string MD4CParser::Impl::ATTR_IS_TASK = "isTask";
537
537
  const std::string MD4CParser::Impl::ATTR_TASK_CHECKED = "taskChecked";
538
538
 
539
- } // namespace Markdown
539
+ } // namespace Markdown
package/cpp/wasm/build.sh CHANGED
@@ -6,17 +6,17 @@
6
6
  # # or follow https://emscripten.org/docs/getting_started/downloads.html
7
7
  #
8
8
  # Usage:
9
- # bash cpp/wasm/build.sh
9
+ # bash packages/core/cpp/wasm/build.sh
10
10
  #
11
11
  # Output:
12
- # src/web/wasm/md4c.js — Emscripten glue with WASM binary inlined as base64
13
- # (SINGLE_FILE=1 means no separate .wasm file is needed)
12
+ # packages/react-native-enriched-markdown/src/web/wasm/md4c.js
14
13
 
15
14
  set -euo pipefail
16
15
 
17
16
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
- PACKAGE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19
- OUT_DIR="$PACKAGE_ROOT/src/web/wasm"
17
+ CPP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
19
+ OUT_DIR="${OUT_DIR:-$REPO_ROOT/packages/react-native-enriched-markdown/src/web/wasm}"
20
20
 
21
21
  mkdir -p "$OUT_DIR"
22
22
 
@@ -24,18 +24,18 @@ echo "Building md4c WASM…"
24
24
 
25
25
  # Compile the C file separately (no -std=c++17)
26
26
  emcc \
27
- -I "$PACKAGE_ROOT/cpp" \
27
+ -I "$CPP_ROOT" \
28
28
  -O2 \
29
- -c "$PACKAGE_ROOT/cpp/md4c/md4c.c" \
29
+ -c "$CPP_ROOT/md4c/md4c.c" \
30
30
  -o "$OUT_DIR/md4c.o"
31
31
 
32
32
  # Compile C++ sources and link everything together
33
33
  emcc \
34
34
  "$SCRIPT_DIR/md4c_wasm.cpp" \
35
35
  "$SCRIPT_DIR/ASTSerializer.cpp" \
36
- "$PACKAGE_ROOT/cpp/parser/MD4CParser.cpp" \
36
+ "$CPP_ROOT/parser/MD4CParser.cpp" \
37
37
  "$OUT_DIR/md4c.o" \
38
- -I "$PACKAGE_ROOT/cpp" \
38
+ -I "$CPP_ROOT" \
39
39
  -I "$SCRIPT_DIR" \
40
40
  -O2 \
41
41
  -std=c++17 \
@@ -0,0 +1,172 @@
1
+ # Accessibility
2
+
3
+ `react-native-enriched-markdown` provides comprehensive accessibility support for screen readers on both iOS and Android platforms.
4
+
5
+ ## Overview
6
+
7
+ The library implements native accessibility features that enable screen readers (VoiceOver on iOS and TalkBack on Android) to properly navigate and understand Markdown content. This includes semantic labeling, custom navigation controls, and proper announcements for all supported elements.
8
+
9
+ ## Text Announcements
10
+
11
+ Plain text paragraphs without inline links or images are announced as a single VoiceOver element per paragraph. Paragraphs containing links or images are segmented into text, link, and image parts so that each remains independently navigable. List items follow the same logic — a list item without inline specials is a single element, while one containing a link is split accordingly. Whitespace-only segments between elements are filtered out to avoid empty announcements.
12
+
13
+ ## Translating announcements — `accessibilityLabels`
14
+
15
+ All strings spoken by the screen reader (list announcements, blockquote suffix, table rows, math equation prefix, and iOS rotor names) can be overridden via the optional `accessibilityLabels` prop on `EnrichedMarkdownText`. The defaults are English; consumers wire in their own i18n pipeline.
16
+
17
+ ```tsx
18
+ import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
19
+
20
+ <EnrichedMarkdownText
21
+ markdown={markdown}
22
+ accessibilityLabels={{
23
+ list: {
24
+ bulletPoint: 'Punkt',
25
+ nestedBulletPoint: 'Eingebetteter Punkt',
26
+ orderedItem: 'Listenelement {n}',
27
+ nestedOrderedItem: 'Eingebettetes Listenelement {n}',
28
+ },
29
+ blockquote: {
30
+ quote: 'Zitat',
31
+ nestedQuote: 'Eingebettetes Zitat',
32
+ },
33
+ table: { row: 'Zeile {n}: {content}' },
34
+ math: { equation: 'Formel: {latex}' },
35
+ rotor: {
36
+ headings: 'Überschriften',
37
+ links: 'Links',
38
+ images: 'Bilder',
39
+ },
40
+ }}
41
+ />
42
+ ```
43
+
44
+ Every field is optional. Any field you omit falls back to the English default below. Defaults are resolved on the JS side before being forwarded to native, so once you set even a single override the rest stay on their built-ins automatically.
45
+
46
+ ### Defaults
47
+
48
+ | Field | Default | Platform |
49
+ |---|---|---|
50
+ | `list.bulletPoint` | `"Bullet point"` | iOS + Android |
51
+ | `list.nestedBulletPoint` | `"Nested bullet point"` | iOS + Android |
52
+ | `list.orderedItem` | `"List item {n}"` | iOS + Android |
53
+ | `list.nestedOrderedItem` | `"Nested list item {n}"` | iOS + Android |
54
+ | `blockquote.quote` | `"Blockquote"` | iOS + Android |
55
+ | `blockquote.nestedQuote` | `"Nested blockquote"` | iOS + Android |
56
+ | `table.row` | `"Row {n}: {content}"` | iOS + Android |
57
+ | `math.equation` | `"Math: {latex}"` | iOS + Android |
58
+ | `rotor.headings` | `"Headings"` | iOS only |
59
+ | `rotor.links` | `"Links"` | iOS only |
60
+ | `rotor.images` | `"Images"` | iOS only |
61
+
62
+ ### Placeholder syntax
63
+
64
+ - `{n}` — 1-based index (list item number, table row index). Substituted at speak time.
65
+ - `{content}` — comma-joined cell texts for a table row.
66
+ - `{latex}` — math equation source.
67
+
68
+ Placeholder names must be preserved in translations exactly. Defaults intentionally use the no-plural cardinal form (`"List item 2"`, `"Row 2"`) instead of ordinals or count-aware variants — a single template works in every language without per-locale plural rules.
69
+
70
+ ## Supported Elements
71
+
72
+ | Element | VoiceOver (iOS) | TalkBack (Android) |
73
+ |---------|-----------------|---------------------|
74
+ | **Headings (h1-h6)** | Rotor navigation, `UIAccessibilityTraitHeader` → "heading" suffix | Reading controls navigation, `isHeading = true` → "heading" suffix |
75
+ | **Links** | Rotor navigation, activatable, "link" suffix | Reading controls navigation, activatable, "link" suffix |
76
+ | **Images** | Alt text announced, rotor navigation, "image" suffix | Alt text announced, "image" role |
77
+ | **Unordered list items** | "Bullet point" / "Nested bullet point" appended (`accessibilityValue`) | Same string appended via `roleDescription` |
78
+ | **Ordered list items** | "List item N" / "Nested list item N" appended | Same string appended via `roleDescription` |
79
+ | **Blockquote content** | "Blockquote" / "Nested blockquote" appended | Same string appended via `roleDescription` |
80
+ | **Table rows** | One focusable element per row, `"Row N: <cells>"` label, header row carries `UIAccessibilityTraitHeader` | One focusable overlay per row, same template, header row carries `setAccessibilityHeading(true)` |
81
+ | **Math equations** | `"Math: <latex>"` (latex read verbatim — no TTS engine) | Same |
82
+
83
+ ## Architecture
84
+
85
+ ### iOS (VoiceOver)
86
+
87
+ **Custom Rotors:**
88
+ - **Headings Rotor**: Navigate between all headings in the document
89
+ - **Links Rotor**: Jump between all links
90
+ - **Images Rotor**: Navigate through all images
91
+
92
+ Rotor names are translated via `accessibilityLabels.rotor.*`. When VoiceOver is active, a two-finger twist on the screen cycles through these rotors; swipe up/down jumps between elements of the currently-selected type.
93
+
94
+ **Semantic Traits:**
95
+ - Headings: `UIAccessibilityTraitHeader | UIAccessibilityTraitStaticText`
96
+ - Links: `UIAccessibilityTraitLink | UIAccessibilityTraitStaticText`
97
+ - Images: `UIAccessibilityTraitImage` (+ `UIAccessibilityTraitLink` if the image is linked)
98
+ - Plain text: `UIAccessibilityTraitStaticText`
99
+
100
+ List, blockquote and table row labels are exposed via `accessibilityValue` so VoiceOver speaks them after the main text.
101
+
102
+ ### Android (TalkBack)
103
+
104
+ **Reading Controls:**
105
+ - Headings, links, and images are available in TalkBack's reading controls for quick navigation
106
+ - List items are properly announced with their position and type (ordered vs unordered)
107
+
108
+ **Accessibility Node Info:**
109
+ - Headings: `isHeading = true` — TalkBack speaks "heading" as the role
110
+ - Links: `isClickable = true` + `roleDescription = "link"`
111
+ - Images: alt text becomes `contentDescription`, `roleDescription = "image"`
112
+ - List items: `CollectionItemInfoCompat` is set so TalkBack also announces position-in-collection; the localized label is appended via `roleDescription`
113
+ - Blockquote content: localized label appended via `roleDescription` (combined with link/list role when nested)
114
+ - Table rows: one transparent overlay per row exposes the row's joined content via `contentDescription`; header rows additionally call `ViewCompat.setAccessibilityHeading(true)`
115
+ - Math: `MathContainerView` exposes the full equation via `contentDescription` (`"Math: <latex>"` template)
116
+
117
+ ## Element Details
118
+
119
+ ### Headings
120
+
121
+ Headings are marked with the platform's "heading" trait/role. Screen readers announce the heading text followed by "heading" — the heading **level** is not spoken by default (the trait already conveys "this is a heading"; spelling the level out adds friction). The level remains available programmatically so platform navigation controls (rotor on iOS, reading controls on Android) can jump between headings of any level.
122
+
123
+ **Example announcement:**
124
+ - "Welcome to Markdown, heading"
125
+ - "Getting Started, heading"
126
+
127
+ ### Links
128
+
129
+ Links are fully interactive and can be activated through screen reader gestures. The link text is announced followed by "link".
130
+
131
+ **Example announcement:**
132
+ - "React Native, link"
133
+
134
+ ### Images
135
+
136
+ Images with alt text announce the alt text + "image". Images without alt text are intentionally **silent** (no fallback string) — if you want them announced, supply alt text in the markdown source.
137
+
138
+ **Example announcement:**
139
+ - "Misty forest at sunrise, image"
140
+ - *(silent for `![](url)`)*
141
+
142
+ ### Lists
143
+
144
+ List items are announced with their position and type:
145
+
146
+ - Unordered: `"<text>, Bullet point"` (top-level) or `"<text>, Nested bullet point"` (deeper)
147
+ - Ordered: `"<text>, List item 1"`, `"<text>, List item 2"`, etc.
148
+
149
+ Override via `accessibilityLabels.list.*`. The `{n}` placeholder is substituted with the 1-based item number.
150
+
151
+ ### Blockquotes
152
+
153
+ Content inside a blockquote gets the blockquote label appended (after any list or link suffix). Nested blockquotes use the `nestedQuote` label instead.
154
+
155
+ **Example announcement:**
156
+ - `"This is a quoted line., Blockquote"`
157
+ - `"Nested quote text., Nested blockquote"`
158
+
159
+ ### Tables
160
+
161
+ Tables expose one focusable element per row (header included), labeled with the `table.row` template. The default reads `"Row 1: Column A, Column B"` for the header, `"Row 2: Cell A1, Cell B1"` for the first body row, and so on. The header row additionally carries the heading trait/role.
162
+
163
+ ### Math
164
+
165
+ Each math block exposes a single focusable element labeled `"Math: <latex>"`. The library does **not** convert LaTeX to natural language — the screen reader reads the raw LaTeX source. If you need spoken math, plug in a LaTeX→speech library on the consumer side and translate the labels accordingly.
166
+
167
+ ## Known Limitations
168
+
169
+ - **Inline formatting** (bold / italic / underline / strikethrough / inline code / spoiler) is not split into separate accessibility elements — the entire paragraph is read as one element, exactly as the surrounding text. Screen readers don't apply visual emphasis to bold/italic by default.
170
+ - **macOS** screen-reader support is still pending — `MarkdownAccessibilityElementBuilder.m` ships a no-op stub for macOS. Tracked as a TODO; iOS implementation can serve as a reference.
171
+ - **Android** has no rotor concept — `accessibilityLabels.rotor.*` is silently ignored on that platform.
172
+ - **iOS** blockquote backgrounds may break at link boundaries instead of spanning the full line. Visual only; doesn't affect accessibility.