react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-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 (163) hide show
  1. package/android/build.gradle +2 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  9. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
  16. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
  20. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
  21. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
  22. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
  23. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
  24. package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
  27. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
  28. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
  29. package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
  30. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
  33. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
  34. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
  36. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
  38. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
  41. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
  42. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
  43. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
  44. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
  45. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +25 -9
  46. package/docs/API_REFERENCE.md +183 -4
  47. package/docs/COPY_OPTIONS.md +40 -0
  48. package/docs/INPUT.md +3 -1
  49. package/docs/MENTIONS.md +8 -1
  50. package/docs/STYLES.md +27 -0
  51. package/docs/TEXT.md +2 -0
  52. package/docs/WEB.md +1 -0
  53. package/ios/EnrichedMarkdown.mm +321 -9
  54. package/ios/EnrichedMarkdownText.mm +203 -6
  55. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
  56. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  57. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  58. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
  59. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
  60. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
  61. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
  62. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  63. package/ios/internals/MeasurementCache.h +8 -2
  64. package/ios/math/ENRMRaTeXBridge.swift +23 -5
  65. package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
  66. package/ios/renderer/LinkRenderer.m +47 -15
  67. package/ios/styles/StyleConfig.h +20 -0
  68. package/ios/styles/StyleConfig.mm +84 -0
  69. package/ios/utils/CodeBackground.m +3 -65
  70. package/ios/utils/ENRMBlockMenu.h +66 -0
  71. package/ios/utils/ENRMChipDrawing.h +24 -0
  72. package/ios/utils/ENRMChipDrawing.m +83 -0
  73. package/ios/utils/ENRMLinkChip.h +70 -0
  74. package/ios/utils/ENRMLinkChip.m +213 -0
  75. package/ios/utils/ENRMLinkLayout.h +50 -0
  76. package/ios/utils/ENRMLinkLayout.m +45 -0
  77. package/ios/utils/ENRMSelectionChange.h +120 -0
  78. package/ios/utils/ENRMSelectionChange.m +282 -0
  79. package/ios/utils/EditMenuUtils+macOS.m +4 -1
  80. package/ios/utils/EditMenuUtils.h +35 -6
  81. package/ios/utils/EditMenuUtils.m +171 -15
  82. package/ios/utils/HTMLGenerator.m +2 -1
  83. package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
  84. package/ios/utils/MarkdownExtractor.m +2 -1
  85. package/ios/utils/RTFExportUtils.m +2 -0
  86. package/ios/utils/StylePropsUtils.h +47 -1
  87. package/ios/utils/TextViewLayoutManager.mm +3 -0
  88. package/ios/views/ENRMCodeBlockContainerView.h +16 -2
  89. package/ios/views/ENRMCodeBlockContainerView.m +22 -19
  90. package/ios/views/ENRMMathContainerView.h +11 -0
  91. package/ios/views/ENRMMathContainerView.m +17 -20
  92. package/ios/views/TableContainerView.h +14 -0
  93. package/ios/views/TableContainerView.m +34 -18
  94. package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
  95. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
  96. package/lib/module/index.js.map +1 -1
  97. package/lib/module/jest/index.js +14 -0
  98. package/lib/module/jest/index.js.map +1 -1
  99. package/lib/module/native/EnrichedMarkdownText.js +169 -11
  100. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  101. package/lib/module/normalizeMarkdownStyle.js +17 -4
  102. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  103. package/lib/module/normalizeMarkdownStyle.web.js +17 -4
  104. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  105. package/lib/module/web/EnrichedMarkdownText.js +6 -3
  106. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  107. package/lib/module/web/renderers/InlineRenderers.js +96 -3
  108. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  109. package/lib/module/web/styles.js +18 -0
  110. package/lib/module/web/styles.js.map +1 -1
  111. package/lib/module/web/utils.js +13 -0
  112. package/lib/module/web/utils.js.map +1 -1
  113. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
  114. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  115. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
  116. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  117. package/lib/typescript/src/index.d.ts +2 -2
  118. package/lib/typescript/src/index.d.ts.map +1 -1
  119. package/lib/typescript/src/index.web.d.ts +1 -1
  120. package/lib/typescript/src/index.web.d.ts.map +1 -1
  121. package/lib/typescript/src/jest/index.d.ts +1 -1
  122. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  123. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  124. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  125. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  126. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  127. package/lib/typescript/src/types/MarkdownStyle.d.ts +50 -5
  128. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  129. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
  130. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  131. package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
  132. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  133. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
  134. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  135. package/lib/typescript/src/types/events.d.ts +66 -0
  136. package/lib/typescript/src/types/events.d.ts.map +1 -1
  137. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  138. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  139. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  140. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  141. package/lib/typescript/src/web/types.d.ts +4 -0
  142. package/lib/typescript/src/web/types.d.ts.map +1 -1
  143. package/lib/typescript/src/web/utils.d.ts +4 -0
  144. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  145. package/package.json +1 -1
  146. package/src/EnrichedMarkdownNativeComponent.ts +138 -1
  147. package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
  148. package/src/index.tsx +6 -0
  149. package/src/index.web.tsx +5 -1
  150. package/src/jest/index.tsx +22 -1
  151. package/src/native/EnrichedMarkdownText.tsx +220 -9
  152. package/src/normalizeMarkdownStyle.ts +15 -0
  153. package/src/normalizeMarkdownStyle.web.ts +13 -0
  154. package/src/types/MarkdownStyle.ts +51 -5
  155. package/src/types/MarkdownStyleInternal.ts +12 -0
  156. package/src/types/MarkdownTextProps.ts +151 -11
  157. package/src/types/MarkdownTextProps.web.ts +43 -1
  158. package/src/types/events.ts +69 -0
  159. package/src/web/EnrichedMarkdownText.tsx +10 -3
  160. package/src/web/renderers/InlineRenderers.tsx +111 -3
  161. package/src/web/styles.ts +25 -0
  162. package/src/web/types.ts +4 -0
  163. package/src/web/utils.ts +8 -0
@@ -0,0 +1,177 @@
1
+ package com.swmansion.enriched.markdown.spans
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.Paint
5
+ import android.graphics.Path
6
+ import android.graphics.RectF
7
+ import android.text.SpannableStringBuilder
8
+ import android.text.Spanned
9
+ import android.text.TextPaint
10
+ import android.text.style.LineBackgroundSpan
11
+ import android.text.style.ReplacementSpan
12
+ import com.swmansion.enriched.markdown.styles.LinkChipStyle
13
+ import kotlin.math.max
14
+ import kotlin.math.min
15
+ import kotlin.math.roundToInt
16
+
17
+ /**
18
+ * Reserves a link's `paddingHorizontal` at one outer edge. It covers one [LinkSpacers.CHAR]
19
+ * inserted by `LinkRenderer`: U+00A0 is line-break class GL, so a break is allowed before it
20
+ * after a space but never after it, and never before it after a non-space — the spacer always
21
+ * travels with the link's first (or last) glyph, never orphaned at a wrap.
22
+ *
23
+ * Layout-only: every text exit point strips spacers by this span ([LinkSpacers.strip]).
24
+ */
25
+ class LinkSpacerSpan(
26
+ private val width: Float,
27
+ ) : ReplacementSpan() {
28
+ override fun getSize(
29
+ paint: Paint,
30
+ text: CharSequence?,
31
+ start: Int,
32
+ end: Int,
33
+ fm: Paint.FontMetricsInt?,
34
+ ): Int {
35
+ // Keep the line metrics of the surrounding text.
36
+ if (fm != null) paint.getFontMetricsInt(fm)
37
+ return width.roundToInt()
38
+ }
39
+
40
+ override fun draw(
41
+ canvas: Canvas,
42
+ text: CharSequence?,
43
+ start: Int,
44
+ end: Int,
45
+ x: Float,
46
+ top: Int,
47
+ y: Int,
48
+ bottom: Int,
49
+ paint: Paint,
50
+ ) = Unit
51
+ }
52
+
53
+ object LinkSpacers {
54
+ const val CHAR = '\u00A0'
55
+
56
+ /** [text] without link spacers (returns [text] itself when it has none). */
57
+ fun strip(text: CharSequence): CharSequence {
58
+ if (text !is Spanned) return text
59
+ val spacers = text.getSpans(0, text.length, LinkSpacerSpan::class.java)
60
+ if (spacers.isEmpty()) return text
61
+ val builder = SpannableStringBuilder(text)
62
+ spacers
63
+ .map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
64
+ .filter { (start, end) -> start in 0 until end }
65
+ .sortedByDescending { it.first }
66
+ .forEach { (start, end) -> builder.delete(start, end) }
67
+ return builder
68
+ }
69
+
70
+ /** True when [start, end) lies inside a spacer (a segment exporters must skip). */
71
+ fun isSpacer(
72
+ text: Spanned,
73
+ start: Int,
74
+ end: Int,
75
+ ): Boolean = end > start && text.getSpans(start, end, LinkSpacerSpan::class.java).isNotEmpty()
76
+ }
77
+
78
+ /**
79
+ * Paints a link chip (background + border) behind the link glyphs. One instance per link,
80
+ * spanning the link including its spacers, so the chip already covers `paddingHorizontal`.
81
+ * Vertically the chip spans the link font's ascent..descent ± `paddingVertical`; the vertical
82
+ * padding only bleeds into the surrounding line spacing and never changes the line box.
83
+ */
84
+ class LinkChipBackgroundSpan(
85
+ val chip: LinkChipStyle,
86
+ ) : LineBackgroundSpan {
87
+ private val rect = RectF()
88
+ private val path = Path()
89
+ private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
90
+ private val borderPaint =
91
+ Paint(Paint.ANTI_ALIAS_FLAG).apply {
92
+ style = Paint.Style.STROKE
93
+ strokeJoin = Paint.Join.ROUND
94
+ strokeCap = Paint.Cap.ROUND
95
+ }
96
+
97
+ override fun drawBackground(
98
+ canvas: Canvas,
99
+ p: Paint,
100
+ left: Int,
101
+ right: Int,
102
+ top: Int,
103
+ baseline: Int,
104
+ bottom: Int,
105
+ text: CharSequence,
106
+ start: Int,
107
+ end: Int,
108
+ lineNum: Int,
109
+ ) {
110
+ if (!chip.isVisible || text !is Spanned) return
111
+
112
+ val spanStart = text.getSpanStart(this)
113
+ val spanEnd = text.getSpanEnd(this)
114
+ if (spanStart !in 0 until spanEnd || spanEnd <= start || spanStart >= end) return
115
+
116
+ val isFirst = spanStart >= start
117
+ val isLast = spanEnd <= end
118
+
119
+ val leadingMargin = InlineChipPainter.leadingMarginAt(text, start)
120
+ val fragmentStart = max(spanStart, start)
121
+ val fragmentEnd = trimTrailingWhitespace(text, fragmentStart, min(spanEnd, end))
122
+ val startX = InlineChipPainter.horizontalOffset(text, start, end, fragmentStart, p, leadingMargin) + left
123
+ val endX = InlineChipPainter.horizontalOffset(text, start, end, fragmentEnd, p, leadingMargin) + left
124
+
125
+ val (ascent, descent) = linkFontMetrics(text, spanStart, spanEnd, p)
126
+ rect.set(
127
+ min(startX, endX),
128
+ baseline + ascent - chip.paddingVertical,
129
+ max(startX, endX),
130
+ baseline + descent + chip.paddingVertical,
131
+ )
132
+
133
+ backgroundPaint.color = chip.backgroundColor
134
+ borderPaint.color = chip.borderColor
135
+ borderPaint.strokeWidth = chip.borderWidth
136
+ InlineChipPainter.draw(
137
+ canvas,
138
+ rect,
139
+ path,
140
+ chip.borderRadius,
141
+ backgroundPaint.takeIf { android.graphics.Color.alpha(chip.backgroundColor) > 0 },
142
+ borderPaint.takeIf { android.graphics.Color.alpha(chip.borderColor) > 0 && chip.borderWidth > 0f },
143
+ isFirst,
144
+ isLast,
145
+ strokeInset = true,
146
+ )
147
+ }
148
+
149
+ companion object {
150
+ /** A wrapped fragment ends before the whitespace the line break hangs. */
151
+ fun trimTrailingWhitespace(
152
+ text: CharSequence,
153
+ start: Int,
154
+ end: Int,
155
+ ): Int {
156
+ var index = end
157
+ while (index > start && text[index - 1] != LinkSpacers.CHAR && text[index - 1].isWhitespace()) index--
158
+ return index
159
+ }
160
+
161
+ /**
162
+ * Ascent/descent of the link's own font: [LinkSpan] sets its size and typeface at draw
163
+ * time only, so the base paint [p] alone would report the paragraph's metrics.
164
+ */
165
+ fun linkFontMetrics(
166
+ text: Spanned,
167
+ spanStart: Int,
168
+ spanEnd: Int,
169
+ p: Paint,
170
+ ): Pair<Float, Float> {
171
+ val paint = TextPaint(p)
172
+ text.getSpans(spanStart, spanEnd, LinkSpan::class.java).firstOrNull()?.updateDrawState(paint)
173
+ val metrics = paint.fontMetrics
174
+ return metrics.ascent to metrics.descent
175
+ }
176
+ }
177
+ }
@@ -1,7 +1,6 @@
1
1
  package com.swmansion.enriched.markdown.spans
2
2
 
3
3
  import android.content.Context
4
- import android.graphics.Color
5
4
  import android.text.TextPaint
6
5
  import android.text.style.ClickableSpan
7
6
  import android.view.View
@@ -57,10 +56,6 @@ class LinkSpan(
57
56
 
58
57
  textPaint.color = variant?.color ?: styleCache.linkColor
59
58
  textPaint.isUnderlineText = variant?.underline ?: styleCache.linkUnderline
60
-
61
- val backgroundColor = variant?.backgroundColor ?: styleCache.linkBackgroundColor
62
- if (Color.alpha(backgroundColor) > 0) {
63
- textPaint.bgColor = backgroundColor
64
- }
59
+ // The background is painted as a chip by LinkChipBackgroundSpan.
65
60
  }
66
61
  }
@@ -1,12 +1,17 @@
1
1
  package com.swmansion.enriched.markdown.styles
2
2
 
3
3
  import com.facebook.react.bridge.ReadableMap
4
+ import kotlin.math.ceil
4
5
 
5
6
  data class InlineMathStyle(
6
7
  val color: Int,
7
8
  val paddingVertical: Float,
8
9
  val fontWeight: String,
10
+ val glyphOverflow: Float = 0f,
9
11
  ) {
12
+ /** Line-box padding the span reports: author padding plus the ink headroom. */
13
+ val boxPaddingVertical: Float get() = paddingVertical + glyphOverflow
14
+
10
15
  companion object {
11
16
  fun fromReadableMap(
12
17
  map: ReadableMap,
@@ -15,7 +20,17 @@ data class InlineMathStyle(
15
20
  val color = parser.parseColor(map, "color")
16
21
  val paddingVertical = parser.toPixelFromDIP(map.getDouble("paddingVertical").toFloat())
17
22
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
18
- return InlineMathStyle(color, paddingVertical, fontWeight)
23
+ val glyphOverflow = glyphOverflowPx(parser.parseOptionalDouble(map, "glyphOverflow").toFloat(), parser)
24
+ return InlineMathStyle(color, paddingVertical, fontWeight, glyphOverflow)
19
25
  }
20
26
  }
21
27
  }
28
+
29
+ /**
30
+ * `glyphOverflow` in whole pixels. Rounded up so a measured box and the bitmap or
31
+ * canvas it holds always grow by the same integer amount on each side.
32
+ */
33
+ internal fun glyphOverflowPx(
34
+ dip: Float,
35
+ parser: StyleParser,
36
+ ): Float = if (dip <= 0f) 0f else ceil(parser.toPixelFromDIP(dip))
@@ -7,6 +7,7 @@ data class LinkStyle(
7
7
  val color: Int,
8
8
  val underline: Boolean,
9
9
  val backgroundColor: Int,
10
+ val chip: LinkChipStyle,
10
11
  ) {
11
12
  companion object {
12
13
  fun fromReadableMap(
@@ -17,7 +18,41 @@ data class LinkStyle(
17
18
  val color = parser.parseColor(map, "color")
18
19
  val underline = map.getBoolean("underline")
19
20
  val backgroundColor = parser.parseColor(map, "backgroundColor")
20
- return LinkStyle(fontFamily, color, underline, backgroundColor)
21
+ return LinkStyle(fontFamily, color, underline, backgroundColor, LinkChipStyle.fromReadableMap(map, parser, backgroundColor))
21
22
  }
22
23
  }
23
24
  }
25
+
26
+ /**
27
+ * Chip ("box") properties of a link, in pixels. Never inherited from the base link
28
+ * style by variants — the JS normalizer fills missing values with 0 / transparent.
29
+ */
30
+ data class LinkChipStyle(
31
+ val backgroundColor: Int,
32
+ val borderColor: Int,
33
+ val borderRadius: Float,
34
+ val borderWidth: Float,
35
+ val paddingHorizontal: Float,
36
+ val paddingVertical: Float,
37
+ ) {
38
+ val isVisible: Boolean
39
+ get() =
40
+ android.graphics.Color.alpha(backgroundColor) > 0 ||
41
+ (android.graphics.Color.alpha(borderColor) > 0 && borderWidth > 0f)
42
+
43
+ companion object {
44
+ fun fromReadableMap(
45
+ map: ReadableMap,
46
+ parser: StyleParser,
47
+ backgroundColor: Int,
48
+ ): LinkChipStyle =
49
+ LinkChipStyle(
50
+ backgroundColor = backgroundColor,
51
+ borderColor = parser.parseOptionalColor(map, "borderColor") ?: 0,
52
+ borderRadius = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "borderRadius").toFloat()),
53
+ borderWidth = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "borderWidth").toFloat()),
54
+ paddingHorizontal = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "paddingHorizontal").toFloat()),
55
+ paddingVertical = parser.toPixelFromDIP(parser.parseOptionalDouble(map, "paddingVertical").toFloat()),
56
+ )
57
+ }
58
+ }
@@ -13,17 +13,21 @@ data class LinkVariantEntry(
13
13
  val color: Int,
14
14
  val underline: Boolean,
15
15
  val backgroundColor: Int,
16
+ val chip: LinkChipStyle,
16
17
  ) {
17
18
  companion object {
18
19
  fun fromReadableMap(
19
20
  map: ReadableMap,
20
21
  parser: StyleParser,
21
- ): LinkVariantEntry =
22
- LinkVariantEntry(
22
+ ): LinkVariantEntry {
23
+ val backgroundColor = parser.parseColor(map, "backgroundColor")
24
+ return LinkVariantEntry(
23
25
  pattern = map.getString("pattern") ?: "",
24
26
  color = parser.parseColor(map, "color"),
25
27
  underline = parser.parseBoolean(map, "underline"),
26
- backgroundColor = parser.parseColor(map, "backgroundColor"),
28
+ backgroundColor = backgroundColor,
29
+ chip = LinkChipStyle.fromReadableMap(map, parser, backgroundColor),
27
30
  )
31
+ }
28
32
  }
29
33
  }
@@ -11,6 +11,7 @@ data class MathStyle(
11
11
  val marginBottom: Float,
12
12
  val textAlign: String,
13
13
  val fontWeight: String,
14
+ val glyphOverflow: Float = 0f,
14
15
  ) {
15
16
  companion object {
16
17
  fun fromReadableMap(
@@ -25,6 +26,7 @@ data class MathStyle(
25
26
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
26
27
  val textAlign = parser.parseString(map, "textAlign", "center")
27
28
  val fontWeight = parser.parseString(map, "fontWeight", "normal")
29
+ val glyphOverflow = glyphOverflowPx(parser.parseOptionalDouble(map, "glyphOverflow").toFloat(), parser)
28
30
  return MathStyle(
29
31
  fontSize,
30
32
  color,
@@ -34,6 +36,7 @@ data class MathStyle(
34
36
  marginBottom,
35
37
  textAlign,
36
38
  fontWeight,
39
+ glyphOverflow,
37
40
  )
38
41
  }
39
42
  }
@@ -7,11 +7,16 @@ import com.facebook.react.uimanager.UIManagerHelper
7
7
  import com.swmansion.enriched.markdown.accessibility.AccessibilityLabels
8
8
  import com.swmansion.enriched.markdown.events.ContextMenuItemPressEvent
9
9
  import com.swmansion.enriched.markdown.events.CopyPressEvent
10
+ import com.swmansion.enriched.markdown.events.CopyRequestEvent
11
+ import com.swmansion.enriched.markdown.events.LinkLayoutEvent
10
12
  import com.swmansion.enriched.markdown.events.LinkLongPressEvent
11
13
  import com.swmansion.enriched.markdown.events.LinkPressEvent
14
+ import com.swmansion.enriched.markdown.events.SelectionChangeEvent
12
15
  import com.swmansion.enriched.markdown.events.TaskListItemPressEvent
13
16
  import com.swmansion.enriched.markdown.parser.Md4cFlags
17
+ import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
14
18
  import com.swmansion.enriched.markdown.utils.text.view.SelectionMenuConfig
19
+ import com.swmansion.enriched.markdown.utils.text.view.SelectionSnapshot
15
20
 
16
21
  fun markdownEventTypeConstants(): MutableMap<String, Any> {
17
22
  val map = mutableMapOf<String, Any>()
@@ -22,11 +27,37 @@ fun markdownEventTypeConstants(): MutableMap<String, Any> {
22
27
  mapOf("registrationName" to TaskListItemPressEvent.EVENT_NAME)
23
28
  map[CopyPressEvent.EVENT_NAME] =
24
29
  mapOf("registrationName" to CopyPressEvent.EVENT_NAME)
30
+ map[CopyRequestEvent.EVENT_NAME] =
31
+ mapOf("registrationName" to CopyRequestEvent.EVENT_NAME)
25
32
  map[ContextMenuItemPressEvent.EVENT_NAME] =
26
33
  mapOf("registrationName" to ContextMenuItemPressEvent.EVENT_NAME)
34
+ map[LinkLayoutEvent.EVENT_NAME] =
35
+ mapOf("registrationName" to LinkLayoutEvent.EVENT_NAME)
36
+ map[SelectionChangeEvent.EVENT_NAME] =
37
+ mapOf("registrationName" to SelectionChangeEvent.EVENT_NAME)
27
38
  return map
28
39
  }
29
40
 
41
+ fun emitLinkLayout(
42
+ view: View,
43
+ fragments: List<LinkLayoutFragment>,
44
+ ) {
45
+ val context = view.context as com.facebook.react.bridge.ReactContext
46
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
47
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
48
+ eventDispatcher?.dispatchEvent(LinkLayoutEvent(surfaceId, view.id, fragments))
49
+ }
50
+
51
+ fun emitSelectionChange(
52
+ view: View,
53
+ snapshot: SelectionSnapshot,
54
+ ) {
55
+ val context = view.context as com.facebook.react.bridge.ReactContext
56
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
57
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
58
+ eventDispatcher?.dispatchEvent(SelectionChangeEvent(surfaceId, view.id, snapshot))
59
+ }
60
+
30
61
  fun emitLinkPress(
31
62
  view: View,
32
63
  url: String,
@@ -74,6 +105,21 @@ fun emitCopyPress(
74
105
  )
75
106
  }
76
107
 
108
+ fun emitCopyRequest(
109
+ view: View,
110
+ source: String,
111
+ markdown: String,
112
+ text: String,
113
+ whole: Boolean,
114
+ ) {
115
+ val context = view.context as com.facebook.react.bridge.ReactContext
116
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
117
+ val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
118
+ eventDispatcher?.dispatchEvent(
119
+ CopyRequestEvent(surfaceId, view.id, source, markdown, text, whole),
120
+ )
121
+ }
122
+
77
123
  fun emitContextMenuItemPress(
78
124
  view: View,
79
125
  itemText: String,
@@ -128,6 +174,11 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
128
174
  copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
129
175
  copyImageUrlsLabel = value.getString("copyImageUrlsLabel") ?: "",
130
176
  copyImageUrlPluralTemplates = parseStringList(value.getArray("copyImageUrlPluralTemplates")),
177
+ copyEnabled = value.getBooleanOrDefault("copyEnabled", true),
178
+ copyFormatMarkdown = value.getBooleanOrDefault("copyFormatMarkdown", false),
179
+ systemItemsNone = value.getBooleanOrDefault("systemItemsNone", false),
180
+ codeBlockCopyButton = value.getBooleanOrDefault("codeBlockCopyButton", true),
181
+ interceptCopy = value.getBooleanOrDefault("interceptCopy", false),
131
182
  )
132
183
  }
133
184
 
@@ -2,10 +2,17 @@ package com.swmansion.enriched.markdown.utils.input
2
2
 
3
3
  /**
4
4
  * The pure half of the paste interception path. A `ClipData` cannot be built on
5
- * the JVM, so the joining rule lives here where a unit test can reach it; the
6
- * two call sites (context menu and `onReceiveContent`) only extract the items.
5
+ * the JVM, so the joining and matching rules live here where a unit test can
6
+ * reach them; the call sites (context menu, `onReceiveContent` and the IME
7
+ * commit) only extract the items.
7
8
  */
8
9
  object PasteTextUtils {
10
+ /**
11
+ * Shortest single-line IME commit that is compared against the clipboard.
12
+ * Longer than a word picked from the suggestion strip.
13
+ */
14
+ const val IME_PASTE_MIN_LENGTH = 16
15
+
9
16
  /**
10
17
  * Flattens a clip's items into one string. A clip regularly carries more than
11
18
  * one item — reading only `getItemAt(0)` silently drops items 2..n. Items
@@ -17,4 +24,29 @@ object PasteTextUtils {
17
24
  .mapNotNull { it?.toString() }
18
25
  .filter { it.isNotEmpty() }
19
26
  .joinToString("\n")
27
+
28
+ /**
29
+ * Whether an IME `commitText` is worth comparing against the clipboard.
30
+ *
31
+ * Reading the clip is not free: on Android 12+ a read can show the system
32
+ * "pasted from your clipboard" toast, so it must not happen while the user is
33
+ * just typing. Typing, autocorrect and suggestion picks commit a word at a
34
+ * time and stay below [IME_PASTE_MIN_LENGTH]. A shorter commit is still
35
+ * compared when it spans lines, since only a paste or a multi-line dictation
36
+ * produces one — but not a lone "\n", which some keyboards commit for Enter.
37
+ */
38
+ fun mayBeImePaste(committed: String): Boolean =
39
+ committed.length >= IME_PASTE_MIN_LENGTH ||
40
+ (committed.length > 1 && committed.contains('\n'))
41
+
42
+ /**
43
+ * Whether an IME commit is the keyboard pasting the clipboard. Only an exact
44
+ * match counts: anything the user typed or dictated differs from the clip.
45
+ * Text the keyboard changed on the way (trimmed, an older clipboard-history
46
+ * entry, one item of a multi-item clip) does not match and inserts inline.
47
+ */
48
+ fun isImePaste(
49
+ committed: String,
50
+ clip: String,
51
+ ): Boolean = clip.isNotEmpty() && clip == committed
20
52
  }
@@ -12,6 +12,7 @@ import com.swmansion.enriched.markdown.spans.EmphasisSpan
12
12
  import com.swmansion.enriched.markdown.spans.HeadingSpan
13
13
  import com.swmansion.enriched.markdown.spans.HighlightSpan
14
14
  import com.swmansion.enriched.markdown.spans.ImageSpan
15
+ import com.swmansion.enriched.markdown.spans.LinkSpacers
15
16
  import com.swmansion.enriched.markdown.spans.LinkSpan
16
17
  import com.swmansion.enriched.markdown.spans.OrderedListSpan
17
18
  import com.swmansion.enriched.markdown.spans.StrikethroughSpan
@@ -676,7 +677,7 @@ object HTMLGenerator {
676
677
  val segmentEnd = minOf(text.nextSpanTransition(i, end, Any::class.java), end)
677
678
  val segmentText = text.subSequence(i, segmentEnd).toString()
678
679
 
679
- if (segmentText.isNotEmpty() && segmentText != "\n") {
680
+ if (segmentText.isNotEmpty() && segmentText != "\n" && !LinkSpacers.isSpacer(text, i, segmentEnd)) {
680
681
  appendStyledSegment(html, text, i, segmentEnd, segmentText, styles, isCodeBlock)
681
682
  }
682
683
  i = segmentEnd
@@ -12,6 +12,7 @@ import com.swmansion.enriched.markdown.spans.EmphasisSpan
12
12
  import com.swmansion.enriched.markdown.spans.HeadingSpan
13
13
  import com.swmansion.enriched.markdown.spans.HighlightSpan
14
14
  import com.swmansion.enriched.markdown.spans.ImageSpan
15
+ import com.swmansion.enriched.markdown.spans.LinkSpacers
15
16
  import com.swmansion.enriched.markdown.spans.LinkSpan
16
17
  import com.swmansion.enriched.markdown.spans.OrderedListSpan
17
18
  import com.swmansion.enriched.markdown.spans.StrikethroughSpan
@@ -55,6 +56,11 @@ object MarkdownExtractor {
55
56
  var i = start
56
57
  while (i < end) {
57
58
  val nextTransition = spannable.nextSpanTransition(i, end, Any::class.java)
59
+ // Link spacers only reserve layout space for chip padding.
60
+ if (LinkSpacers.isSpacer(spannable, i, nextTransition)) {
61
+ i = nextTransition
62
+ continue
63
+ }
58
64
  val segmentText = spannable.subSequence(i, nextTransition).toString()
59
65
 
60
66
  val handled =
@@ -0,0 +1,88 @@
1
+ package com.swmansion.enriched.markdown.utils.text.view
2
+
3
+ import android.view.ActionMode
4
+ import android.view.Menu
5
+ import android.view.MenuInflater
6
+ import android.view.View
7
+ import android.widget.PopupMenu
8
+
9
+ /**
10
+ * Stands in for the floating text-selection toolbar while `contextMenuHidden`
11
+ * is on. An empty menu only hides the AOSP FloatingToolbar; ROMs that draw
12
+ * their own selection popup (MIUI) still show it. Answering the Editor's
13
+ * startActionMode with this mode means no toolbar window is ever created.
14
+ *
15
+ * The callback is the Editor's TextActionModeCallback wrapping ours, so it
16
+ * still gets the full create/prepare/destroy lifecycle: Editor keeps the
17
+ * selection handles and its transient-state bookkeeping, and finish() clears
18
+ * the selection exactly as dismissing the real toolbar would.
19
+ */
20
+ internal class HeadlessActionMode private constructor(
21
+ private val view: View,
22
+ private val callback: ActionMode.Callback,
23
+ ) : ActionMode() {
24
+ // A real framework menu: Editor populates it with MenuItem calls
25
+ // (setAlphabeticShortcut, setShowAsAction, setIntent) a stub can't take.
26
+ private val menu: Menu = PopupMenu(view.context, view).menu
27
+ private var finished = false
28
+
29
+ override fun setTitle(title: CharSequence?) = Unit
30
+
31
+ override fun setTitle(resId: Int) = Unit
32
+
33
+ override fun setSubtitle(subtitle: CharSequence?) = Unit
34
+
35
+ override fun setSubtitle(resId: Int) = Unit
36
+
37
+ override fun setCustomView(view: View?) = Unit
38
+
39
+ override fun invalidate() {
40
+ if (!finished) callback.onPrepareActionMode(this, menu)
41
+ }
42
+
43
+ override fun invalidateContentRect() = Unit
44
+
45
+ override fun hide(duration: Long) = Unit
46
+
47
+ override fun finish() {
48
+ if (finished) return
49
+ finished = true
50
+ callback.onDestroyActionMode(this)
51
+ }
52
+
53
+ override fun getMenu(): Menu = menu
54
+
55
+ override fun getTitle(): CharSequence? = null
56
+
57
+ override fun getSubtitle(): CharSequence? = null
58
+
59
+ override fun getCustomView(): View? = null
60
+
61
+ override fun getMenuInflater(): MenuInflater = MenuInflater(view.context)
62
+
63
+ companion object {
64
+ /** Mirrors the framework: null when the callback declines to start. */
65
+ fun start(
66
+ view: View,
67
+ callback: ActionMode.Callback,
68
+ ): ActionMode? {
69
+ val mode = HeadlessActionMode(view, callback)
70
+ mode.type = TYPE_FLOATING
71
+ if (!callback.onCreateActionMode(mode, mode.menu)) return null
72
+ callback.onPrepareActionMode(mode, mode.menu)
73
+ return mode
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * MIUI 12 (Android 10) replaces android.widget.Editor: text selection never reaches
80
+ * startActionMode or the custom ActionMode callback, so HeadlessActionMode is never used.
81
+ * Its own popup is shown by Editor$HandleView.showActionPopupWindow, which posts a runnable
82
+ * on the TextView (Editor$HandleView$2). Dropping that runnable is the only hook left while
83
+ * `contextMenuHidden` is on.
84
+ */
85
+ private const val ROM_SELECTION_POPUP_SHOWER_PREFIX = "android.widget.Editor\$HandleView"
86
+
87
+ internal fun isRomSelectionPopupShower(action: Runnable?): Boolean =
88
+ action != null && action.javaClass.name.startsWith(ROM_SELECTION_POPUP_SHOWER_PREFIX)