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.
- package/ReactNativeEnrichedMarkdown.podspec +62 -11
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +9 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +20 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +744 -0
- package/android/src/main/baseline-prof.txt +75 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +10 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +9 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/AccessibilityLabels.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +44 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +89 -11
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +22 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputRemend.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +54 -13
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +4 -4
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkEvents.kt +22 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +36 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +73 -3
- package/android/src/main/jni/CMakeLists.txt +15 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +24 -2
- package/app.plugin.js +2 -1
- package/cpp/parser/MD4CParser.cpp +1 -1
- package/cpp/wasm/build.sh +9 -9
- package/docs/ACCESSIBILITY.md +172 -0
- package/docs/API_REFERENCE.md +948 -0
- package/docs/COPY_OPTIONS.md +127 -0
- package/docs/ELEMENTS_STRUCTURE.md +112 -0
- package/docs/IMAGE_CACHING.md +25 -0
- package/docs/INPUT.md +233 -0
- package/docs/LATEX_MATH.md +136 -0
- package/docs/MACOS.md +19 -0
- package/docs/MARKDOWN_STREAMING.md +36 -0
- package/docs/MENTIONS.md +97 -0
- package/docs/RTL.md +82 -0
- package/docs/STYLES.md +519 -0
- package/docs/TEXT.md +110 -0
- package/docs/WEB.md +49 -0
- package/ios/EnrichedMarkdown.mm +82 -25
- package/ios/EnrichedMarkdownText.mm +37 -5
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +744 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +15 -0
- package/ios/input/ENRMFormattingStore.h +5 -0
- package/ios/input/ENRMFormattingStore.mm +34 -0
- package/ios/input/ENRMInputTextView.h +2 -0
- package/ios/input/ENRMInputTextView.mm +8 -17
- package/ios/input/ENRMMarkdownSerializer.mm +1 -1
- package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +71 -53
- package/ios/input/EnrichedMarkdownTextInput+Internal.h +26 -0
- package/ios/input/EnrichedMarkdownTextInput.h +1 -0
- package/ios/input/EnrichedMarkdownTextInput.mm +230 -29
- package/ios/input/styles/ENRMBoldStyleHandler.mm +3 -2
- package/ios/input/styles/ENRMItalicStyleHandler.mm +3 -2
- package/ios/renderer/AttributedRenderer.m +51 -6
- package/ios/renderer/ENRMMathInlineRenderer.m +5 -1
- package/ios/styles/ENRMFontSlot.h +1 -1
- package/ios/utils/ENRMAccessibilityLabels.h +36 -0
- package/ios/utils/ENRMAccessibilityLabels.m +5 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -5
- package/ios/utils/EditMenuUtils.h +70 -0
- package/ios/utils/EditMenuUtils.m +14 -11
- package/ios/utils/LastElementUtils.h +39 -2
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +18 -16
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +64 -32
- package/ios/utils/ParagraphStyleUtils.m +21 -2
- package/ios/views/ENRMMathContainerView.h +8 -0
- package/ios/views/ENRMMathContainerView.m +7 -5
- package/ios/views/ENRMTableIOSGridView.h +40 -0
- package/ios/views/ENRMTableIOSGridView.m +216 -0
- package/ios/views/EnrichedMarkdownInternalText.h +2 -0
- package/ios/views/EnrichedMarkdownInternalText.m +2 -0
- package/ios/views/TableContainerView.h +8 -0
- package/ios/views/TableContainerView.m +72 -128
- package/lib/module/EnrichedMarkdownNativeComponent.ts +37 -0
- package/lib/module/EnrichedMarkdownTextInput.js +48 -1
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +35 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +37 -0
- package/lib/module/accessibilityLabelDefaults.js +64 -0
- package/lib/module/accessibilityLabelDefaults.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +72 -4
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMenuItem.js +38 -0
- package/lib/module/normalizeMenuItem.js.map +1 -0
- package/lib/module/types/AccessibilityLabels.js +2 -0
- package/lib/module/types/AccessibilityLabels.js.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +34 -0
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +55 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +30 -0
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +34 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/accessibilityLabelDefaults.d.ts +16 -0
- package/lib/typescript/src/accessibilityLabelDefaults.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMenuItem.d.ts +7 -0
- package/lib/typescript/src/normalizeMenuItem.d.ts.map +1 -0
- package/lib/typescript/src/types/AccessibilityLabels.d.ts +114 -0
- package/lib/typescript/src/types/AccessibilityLabels.d.ts.map +1 -0
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +78 -9
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/package.json +4 -2
- package/plugin/build/withAndroidMath.js +19 -0
- package/plugin/build/withIosMath.js +28 -0
- package/plugin/build/withReactNativeEnrichedMarkdown.js +11 -0
- package/plugin/tsconfig.build.json +16 -0
- package/src/EnrichedMarkdownNativeComponent.ts +37 -0
- package/src/EnrichedMarkdownTextInput.tsx +117 -1
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +35 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +37 -0
- package/src/accessibilityLabelDefaults.ts +83 -0
- package/src/index.tsx +11 -0
- package/src/native/EnrichedMarkdownText.tsx +120 -6
- package/src/normalizeMenuItem.ts +52 -0
- package/src/types/AccessibilityLabels.ts +118 -0
- package/src/types/MarkdownTextProps.ts +74 -9
- package/lib/module/plugin/withAndroidMath.js +0 -20
- package/lib/module/plugin/withAndroidMath.js.map +0 -1
- package/lib/module/plugin/withIosMath.js +0 -23
- package/lib/module/plugin/withIosMath.js.map +0 -1
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js +0 -16
- package/lib/module/plugin/withReactNativeEnrichedMarkdown.js.map +0 -1
- package/lib/typescript/src/plugin/withAndroidMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withAndroidMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withIosMath.d.ts +0 -5
- package/lib/typescript/src/plugin/withIosMath.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts +0 -6
- package/lib/typescript/src/plugin/withReactNativeEnrichedMarkdown.d.ts.map +0 -1
- /package/{src/plugin → plugin/src}/withAndroidMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withIosMath.ts +0 -0
- /package/{src/plugin → plugin/src}/withReactNativeEnrichedMarkdown.ts +0 -0
package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
120
|
+
item(ContextMenuPopup.Icon.COPY, copyLabel) {
|
|
99
121
|
clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
|
|
100
122
|
}
|
|
101
|
-
item(ContextMenuPopup.Icon.DOCUMENT,
|
|
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 =
|
|
1
|
+
module.exports =
|
|
2
|
+
require('./plugin/build/withReactNativeEnrichedMarkdown').default;
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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 "$
|
|
27
|
+
-I "$CPP_ROOT" \
|
|
28
28
|
-O2 \
|
|
29
|
-
-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
|
-
"$
|
|
36
|
+
"$CPP_ROOT/parser/MD4CParser.cpp" \
|
|
37
37
|
"$OUT_DIR/md4c.o" \
|
|
38
|
-
-I "$
|
|
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 ``)*
|
|
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.
|