react-native-enriched 0.2.0 → 0.3.0

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 (186) hide show
  1. package/README.md +16 -17
  2. package/android/build.gradle +77 -72
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +21 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +7 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +156 -0
  6. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +147 -0
  7. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +194 -0
  9. package/android/lint.gradle +70 -0
  10. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputConnectionWrapper.kt +140 -0
  11. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +304 -83
  12. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +3 -1
  13. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +166 -51
  14. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +1 -3
  15. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +70 -21
  16. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +21 -11
  17. package/android/src/main/java/com/swmansion/enriched/events/OnChangeHtmlEvent.kt +8 -9
  18. package/android/src/main/java/com/swmansion/enriched/events/OnChangeSelectionEvent.kt +10 -9
  19. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateDeprecatedEvent.kt +21 -0
  20. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateEvent.kt +9 -12
  21. package/android/src/main/java/com/swmansion/enriched/events/OnChangeTextEvent.kt +10 -10
  22. package/android/src/main/java/com/swmansion/enriched/events/OnInputBlurEvent.kt +7 -9
  23. package/android/src/main/java/com/swmansion/enriched/events/OnInputFocusEvent.kt +7 -9
  24. package/android/src/main/java/com/swmansion/enriched/events/OnInputKeyPressEvent.kt +27 -0
  25. package/android/src/main/java/com/swmansion/enriched/events/OnLinkDetectedEvent.kt +13 -11
  26. package/android/src/main/java/com/swmansion/enriched/events/OnMentionDetectedEvent.kt +10 -9
  27. package/android/src/main/java/com/swmansion/enriched/events/OnMentionEvent.kt +9 -8
  28. package/android/src/main/java/com/swmansion/enriched/events/OnRequestHtmlResultEvent.kt +32 -0
  29. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +24 -5
  30. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +8 -1
  31. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +10 -2
  32. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +8 -1
  33. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +8 -1
  34. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +8 -1
  35. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH4Span.kt +24 -0
  36. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH5Span.kt +24 -0
  37. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH6Span.kt +24 -0
  38. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +34 -17
  39. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +8 -1
  40. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +7 -1
  41. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +10 -4
  42. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +14 -11
  43. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +18 -11
  44. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +174 -72
  45. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +7 -1
  46. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +7 -1
  47. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -5
  48. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedBlockSpan.kt +3 -2
  49. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedHeadingSpan.kt +1 -2
  50. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedInlineSpan.kt +1 -2
  51. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedParagraphSpan.kt +3 -2
  52. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +5 -0
  53. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +1 -2
  54. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +2 -1
  55. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +155 -20
  56. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +25 -8
  57. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +60 -20
  58. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +161 -25
  59. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +128 -52
  60. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +10 -7
  61. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedConstants.kt +11 -0
  62. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedEditableFactory.kt +17 -0
  63. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +136 -87
  64. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +71 -42
  65. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt +183 -48
  66. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpannable.kt +82 -0
  67. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpannableStringBuilder.kt +15 -0
  68. package/android/src/main/java/com/swmansion/enriched/utils/Utils.kt +0 -70
  69. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +46 -14
  70. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +34 -11
  71. package/android/src/main/new_arch/CMakeLists.txt +6 -0
  72. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +6 -6
  73. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +6 -6
  74. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +19 -19
  75. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +40 -51
  76. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +13 -15
  77. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +23 -21
  78. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +35 -36
  79. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +4 -4
  80. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +13 -14
  81. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +33 -14
  82. package/ios/EnrichedTextInputView.h +26 -14
  83. package/ios/EnrichedTextInputView.mm +1209 -586
  84. package/ios/config/InputConfig.h +24 -6
  85. package/ios/config/InputConfig.mm +154 -38
  86. package/ios/{utils → extensions}/ColorExtension.mm +7 -5
  87. package/ios/extensions/FontExtension.mm +106 -0
  88. package/ios/{utils → extensions}/LayoutManagerExtension.h +1 -1
  89. package/ios/extensions/LayoutManagerExtension.mm +396 -0
  90. package/ios/{utils → extensions}/StringExtension.mm +19 -16
  91. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +156 -0
  92. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +147 -0
  93. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +10 -0
  94. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +194 -0
  95. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +95 -0
  96. package/ios/inputParser/InputParser.h +5 -5
  97. package/ios/inputParser/InputParser.mm +864 -380
  98. package/ios/inputTextView/InputTextView.h +1 -1
  99. package/ios/inputTextView/InputTextView.mm +100 -59
  100. package/ios/{utils → interfaces}/BaseStyleProtocol.h +2 -2
  101. package/ios/interfaces/ImageAttachment.h +10 -0
  102. package/ios/interfaces/ImageAttachment.mm +36 -0
  103. package/ios/interfaces/LinkRegexConfig.h +19 -0
  104. package/ios/interfaces/LinkRegexConfig.mm +37 -0
  105. package/ios/interfaces/MediaAttachment.h +23 -0
  106. package/ios/interfaces/MediaAttachment.mm +31 -0
  107. package/ios/{utils → interfaces}/MentionParams.h +0 -1
  108. package/ios/{utils → interfaces}/MentionStyleProps.mm +27 -20
  109. package/ios/{utils → interfaces}/StyleHeaders.h +37 -15
  110. package/ios/{utils → interfaces}/StyleTypeEnum.h +3 -0
  111. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +11 -9
  112. package/ios/internals/EnrichedTextInputViewShadowNode.h +28 -25
  113. package/ios/internals/EnrichedTextInputViewShadowNode.mm +45 -40
  114. package/ios/internals/EnrichedTextInputViewState.h +3 -1
  115. package/ios/styles/BlockQuoteStyle.mm +189 -118
  116. package/ios/styles/BoldStyle.mm +110 -63
  117. package/ios/styles/CodeBlockStyle.mm +204 -128
  118. package/ios/styles/H1Style.mm +10 -4
  119. package/ios/styles/H2Style.mm +10 -4
  120. package/ios/styles/H3Style.mm +10 -4
  121. package/ios/styles/H4Style.mm +17 -0
  122. package/ios/styles/H5Style.mm +17 -0
  123. package/ios/styles/H6Style.mm +17 -0
  124. package/ios/styles/HeadingStyleBase.mm +148 -86
  125. package/ios/styles/ImageStyle.mm +75 -73
  126. package/ios/styles/InlineCodeStyle.mm +162 -88
  127. package/ios/styles/ItalicStyle.mm +76 -52
  128. package/ios/styles/LinkStyle.mm +411 -232
  129. package/ios/styles/MentionStyle.mm +363 -246
  130. package/ios/styles/OrderedListStyle.mm +171 -106
  131. package/ios/styles/StrikethroughStyle.mm +52 -35
  132. package/ios/styles/UnderlineStyle.mm +68 -46
  133. package/ios/styles/UnorderedListStyle.mm +169 -106
  134. package/ios/utils/OccurenceUtils.h +42 -42
  135. package/ios/utils/OccurenceUtils.mm +142 -119
  136. package/ios/utils/ParagraphAttributesUtils.h +10 -2
  137. package/ios/utils/ParagraphAttributesUtils.mm +182 -71
  138. package/ios/utils/ParagraphsUtils.h +2 -1
  139. package/ios/utils/ParagraphsUtils.mm +41 -27
  140. package/ios/utils/TextInsertionUtils.h +13 -2
  141. package/ios/utils/TextInsertionUtils.mm +38 -20
  142. package/ios/utils/WordsUtils.h +2 -1
  143. package/ios/utils/WordsUtils.mm +32 -22
  144. package/ios/utils/ZeroWidthSpaceUtils.h +3 -1
  145. package/ios/utils/ZeroWidthSpaceUtils.mm +145 -79
  146. package/lib/module/EnrichedTextInput.js +61 -2
  147. package/lib/module/EnrichedTextInput.js.map +1 -1
  148. package/lib/module/EnrichedTextInputNativeComponent.ts +149 -12
  149. package/lib/module/{normalizeHtmlStyle.js → utils/normalizeHtmlStyle.js} +12 -0
  150. package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
  151. package/lib/module/utils/regexParser.js +46 -0
  152. package/lib/module/utils/regexParser.js.map +1 -0
  153. package/lib/typescript/src/EnrichedTextInput.d.ts +24 -14
  154. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  155. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +129 -12
  156. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  157. package/lib/typescript/src/index.d.ts +1 -1
  158. package/lib/typescript/src/index.d.ts.map +1 -1
  159. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
  160. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
  161. package/lib/typescript/src/utils/regexParser.d.ts +3 -0
  162. package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
  163. package/package.json +17 -6
  164. package/src/EnrichedTextInput.tsx +96 -13
  165. package/src/EnrichedTextInputNativeComponent.ts +149 -12
  166. package/src/index.tsx +2 -0
  167. package/src/{normalizeHtmlStyle.ts → utils/normalizeHtmlStyle.ts} +14 -2
  168. package/src/utils/regexParser.ts +56 -0
  169. package/ios/utils/FontExtension.mm +0 -91
  170. package/ios/utils/LayoutManagerExtension.mm +0 -286
  171. package/lib/module/normalizeHtmlStyle.js.map +0 -1
  172. package/lib/typescript/src/normalizeHtmlStyle.d.ts +0 -4
  173. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +0 -1
  174. package/ios/{utils → extensions}/ColorExtension.h +0 -0
  175. package/ios/{utils → extensions}/FontExtension.h +0 -0
  176. package/ios/{utils → extensions}/StringExtension.h +1 -1
  177. package/ios/{utils → interfaces}/ImageData.h +0 -0
  178. package/ios/{utils → interfaces}/ImageData.mm +0 -0
  179. package/ios/{utils → interfaces}/LinkData.h +0 -0
  180. package/ios/{utils → interfaces}/LinkData.mm +0 -0
  181. package/ios/{utils → interfaces}/MentionParams.mm +0 -0
  182. package/ios/{utils → interfaces}/MentionStyleProps.h +1 -1
  183. /package/ios/{utils → interfaces}/StylePair.h +0 -0
  184. /package/ios/{utils → interfaces}/StylePair.mm +0 -0
  185. /package/ios/{utils → interfaces}/TextDecorationLineEnum.h +0 -0
  186. /package/ios/{utils → interfaces}/TextDecorationLineEnum.mm +0 -0
@@ -5,14 +5,21 @@ import android.text.Spannable
5
5
  import android.text.SpannableStringBuilder
6
6
  import android.util.Log
7
7
  import com.swmansion.enriched.EnrichedTextInputView
8
- import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan
9
- import com.swmansion.enriched.spans.EnrichedCodeBlockSpan
10
8
  import com.swmansion.enriched.spans.EnrichedSpans
9
+ import com.swmansion.enriched.spans.interfaces.EnrichedSpan
10
+ import com.swmansion.enriched.utils.EnrichedConstants
11
11
  import com.swmansion.enriched.utils.getParagraphBounds
12
12
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
13
-
14
- class ParagraphStyles(private val view: EnrichedTextInputView) {
15
- private fun <T>getPreviousParagraphSpan(spannable: Spannable, paragraphStart: Int, type: Class<T>): T? {
13
+ import com.swmansion.enriched.utils.removeZWS
14
+
15
+ class ParagraphStyles(
16
+ private val view: EnrichedTextInputView,
17
+ ) {
18
+ private fun <T> getPreviousParagraphSpan(
19
+ spannable: Spannable,
20
+ paragraphStart: Int,
21
+ type: Class<T>,
22
+ ): T? {
16
23
  if (paragraphStart <= 0) return null
17
24
 
18
25
  val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(paragraphStart - 1)
@@ -31,7 +38,11 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
31
38
  return null
32
39
  }
33
40
 
34
- private fun <T>getNextParagraphSpan(spannable: Spannable, paragraphEnd: Int, type: Class<T>): T? {
41
+ private fun <T> getNextParagraphSpan(
42
+ spannable: Spannable,
43
+ paragraphEnd: Int,
44
+ type: Class<T>,
45
+ ): T? {
35
46
  if (paragraphEnd >= spannable.length - 1) return null
36
47
 
37
48
  val (nextParagraphStart, nextParagraphEnd) = spannable.getParagraphBounds(paragraphEnd + 1)
@@ -55,7 +66,12 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
55
66
  * Applies a continuous span to the specified range.
56
67
  * If the new range touches existing continuous spans, they are coalesced into a single span
57
68
  */
58
- private fun <T>setContinuousSpan(spannable: Spannable, start: Int, end: Int, type: Class<T>) {
69
+ private fun <T> setContinuousSpan(
70
+ spannable: Spannable,
71
+ start: Int,
72
+ end: Int,
73
+ type: Class<T>,
74
+ ) {
59
75
  val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
60
76
  val previousSpan = getPreviousParagraphSpan(spannable, start, type)
61
77
  val nextSpan = getNextParagraphSpan(spannable, end, type)
@@ -76,8 +92,12 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
76
92
  spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
77
93
  }
78
94
 
79
-
80
- private fun <T>setSpan(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
95
+ private fun <T> setSpan(
96
+ spannable: Spannable,
97
+ type: Class<T>,
98
+ start: Int,
99
+ end: Int,
100
+ ) {
81
101
  if (EnrichedSpans.isTypeContinuous(type)) {
82
102
  setContinuousSpan(spannable, start, end, type)
83
103
  return
@@ -88,7 +108,12 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
88
108
  spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
89
109
  }
90
110
 
91
- private fun <T>removeSpansForRange(spannable: Spannable, start: Int, end: Int, clazz: Class<T>): Boolean {
111
+ private fun <T> removeSpansForRange(
112
+ spannable: Spannable,
113
+ start: Int,
114
+ end: Int,
115
+ clazz: Class<T>,
116
+ ): Boolean {
92
117
  val ssb = spannable as SpannableStringBuilder
93
118
  var finalStart = start
94
119
  var finalEnd = end
@@ -103,11 +128,17 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
103
128
  ssb.removeSpan(span)
104
129
  }
105
130
 
106
- ssb.replace(finalStart, finalEnd, ssb.substring(finalStart, finalEnd).replace("\u200B", ""))
131
+ ssb.removeZWS(finalStart, finalEnd)
132
+
107
133
  return true
108
134
  }
109
135
 
110
- private fun <T>setAndMergeSpans(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
136
+ private fun <T> setAndMergeSpans(
137
+ spannable: Spannable,
138
+ type: Class<T>,
139
+ start: Int,
140
+ end: Int,
141
+ ) {
111
142
  val spans = spannable.getSpans(start, end, type)
112
143
 
113
144
  // No spans setup for current selection, means we just need to assign new span
@@ -158,7 +189,11 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
158
189
  }
159
190
  }
160
191
 
161
- private fun <T>isSpanEnabledInNextLine(spannable: Spannable, index: Int, type: Class<T>): Boolean {
192
+ private fun <T> isSpanEnabledInNextLine(
193
+ spannable: Spannable,
194
+ index: Int,
195
+ type: Class<T>,
196
+ ): Boolean {
162
197
  val selection = view.selection ?: return false
163
198
  if (index + 1 >= spannable.length) return false
164
199
  val (start, end) = selection.getParagraphSelection()
@@ -167,7 +202,11 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
167
202
  return spans.isNotEmpty()
168
203
  }
169
204
 
170
- private fun <T>mergeAdjacentStyleSpans(s: Editable, endCursorPosition: Int, type: Class<T>) {
205
+ private fun <T> mergeAdjacentStyleSpans(
206
+ s: Editable,
207
+ endCursorPosition: Int,
208
+ type: Class<T>,
209
+ ) {
171
210
  val (start, end) = s.getParagraphBounds(endCursorPosition)
172
211
  val currParagraphSpans = s.getSpans(start, end, type)
173
212
 
@@ -194,16 +233,102 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
194
233
  s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
195
234
  }
196
235
 
197
- fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
236
+ private fun handleConflictsDuringNewlineDeletion(
237
+ s: Editable,
238
+ style: String,
239
+ paragraphStart: Int,
240
+ paragraphEnd: Int,
241
+ ): Boolean {
242
+ val spanState = view.spanState ?: return false
243
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return false
244
+ var isConflicting = false
245
+ val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
246
+
247
+ for (styleToCheck in stylesToCheck) {
248
+ val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
249
+
250
+ val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
251
+ if (spans.isEmpty()) {
252
+ continue
253
+ }
254
+ isConflicting = true
255
+
256
+ val isParagraphStyle = EnrichedSpans.paragraphSpans[styleToCheck] != null
257
+ if (!isParagraphStyle) {
258
+ continue
259
+ }
260
+
261
+ for (span in spans) {
262
+ extendStyleOnWholeParagraph(s, span as EnrichedSpan, conflictingType, paragraphEnd)
263
+ }
264
+ }
265
+
266
+ if (isConflicting) {
267
+ val styleStart = spanState.getStart(style) ?: return false
268
+ spanState.setStart(style, null)
269
+ removeStyle(style, styleStart, paragraphEnd)
270
+ return true
271
+ }
272
+
273
+ return false
274
+ }
275
+
276
+ private fun deleteConflictingAndBlockingStyles(
277
+ s: Editable,
278
+ style: String,
279
+ paragraphStart: Int,
280
+ paragraphEnd: Int,
281
+ ) {
282
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return
283
+ val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
284
+
285
+ for (styleToCheck in stylesToCheck) {
286
+ val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
287
+
288
+ val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
289
+ for (span in spans) {
290
+ s.removeSpan(span)
291
+ }
292
+ }
293
+ }
294
+
295
+ private fun <T> extendStyleOnWholeParagraph(
296
+ s: Editable,
297
+ span: EnrichedSpan,
298
+ type: Class<T>,
299
+ paragraphEnd: Int,
300
+ ) {
301
+ val currStyleStart = s.getSpanStart(span)
302
+ s.removeSpan(span)
303
+ val (safeStart, safeEnd) = s.getSafeSpanBoundaries(currStyleStart, paragraphEnd)
304
+ setSpan(s, type, safeStart, safeEnd)
305
+ }
306
+
307
+ fun afterTextChanged(
308
+ s: Editable,
309
+ endPosition: Int,
310
+ previousTextLength: Int,
311
+ ) {
198
312
  var endCursorPosition = endPosition
199
313
  val isBackspace = s.length < previousTextLength
200
- val isNewLine = endCursorPosition == 0 || endCursorPosition > 0 && s[endCursorPosition - 1] == '\n'
314
+ val isNewLine = endCursorPosition == 0 || (endCursorPosition > 0 && s[endCursorPosition - 1] == '\n')
315
+ val spanState = view.spanState ?: return
201
316
 
202
317
  for ((style, config) in EnrichedSpans.paragraphSpans) {
203
- val spanState = view.spanState ?: continue
204
318
  val styleStart = spanState.getStart(style)
205
319
 
206
320
  if (styleStart == null) {
321
+ if (isBackspace) {
322
+ val (start, end) = s.getParagraphBounds(endCursorPosition)
323
+ val spans = s.getSpans(start, end, config.clazz)
324
+
325
+ for (span in spans) {
326
+ // handle conflicts when entering paragraph with some paragraph style applied
327
+ deleteConflictingAndBlockingStyles(s, style, start, end)
328
+ extendStyleOnWholeParagraph(s, span as EnrichedSpan, config.clazz, end)
329
+ }
330
+ }
331
+
207
332
  if (config.isContinuous) {
208
333
  mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
209
334
  }
@@ -218,14 +343,23 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
218
343
 
219
344
  if (isBackspace) {
220
345
  endCursorPosition -= 1
221
- view.spanState.setStart(style, null)
346
+ spanState.setStart(style, null)
222
347
  } else {
223
- s.insert(endCursorPosition, "\u200B")
348
+ s.insert(endCursorPosition, EnrichedConstants.ZWS_STRING)
224
349
  endCursorPosition += 1
225
350
  }
226
351
  }
227
352
 
228
353
  var (start, end) = s.getParagraphBounds(styleStart, endCursorPosition)
354
+
355
+ // handle conflicts when deleting newline from paragraph style (going back to previous line)
356
+ if (isBackspace && styleStart != start) {
357
+ val isConflicting = handleConflictsDuringNewlineDeletion(s, style, start, end)
358
+ if (isConflicting) {
359
+ continue
360
+ }
361
+ }
362
+
229
363
  val isNotEndLineSpan = isSpanEnabledInNextLine(s, end, config.clazz)
230
364
  val spans = s.getSpans(start, end, config.clazz)
231
365
 
@@ -260,7 +394,7 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
260
394
  }
261
395
 
262
396
  if (start == end) {
263
- spannable.insert(start, "\u200B")
397
+ spannable.insert(start, EnrichedConstants.ZWS_STRING)
264
398
  setAndMergeSpans(spannable, type, start, end + 1)
265
399
  view.selection.validateStyles()
266
400
 
@@ -272,7 +406,7 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
272
406
  val paragraphs = spannable.substring(start, end).split("\n")
273
407
 
274
408
  for (paragraph in paragraphs) {
275
- spannable.insert(currentStart, "\u200B")
409
+ spannable.insert(currentStart, EnrichedConstants.ZWS_STRING)
276
410
  currentEnd = currentStart + paragraph.length + 1
277
411
  currentStart = currentEnd + 1
278
412
  }
@@ -281,11 +415,13 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
281
415
  view.selection.validateStyles()
282
416
  }
283
417
 
284
- fun getStyleRange(): Pair<Int, Int> {
285
- return view.selection?.getParagraphSelection() ?: Pair(0, 0)
286
- }
418
+ fun getStyleRange(): Pair<Int, Int> = view.selection?.getParagraphSelection() ?: Pair(0, 0)
287
419
 
288
- fun removeStyle(name: String, start: Int, end: Int): Boolean {
420
+ fun removeStyle(
421
+ name: String,
422
+ start: Int,
423
+ end: Int,
424
+ ): Boolean {
289
425
  val config = EnrichedSpans.paragraphSpans[name] ?: return false
290
426
  val spannable = view.text as Spannable
291
427
  return removeSpansForRange(spannable, start, end, config.clazz)
@@ -9,20 +9,29 @@ import com.swmansion.enriched.spans.EnrichedImageSpan
9
9
  import com.swmansion.enriched.spans.EnrichedLinkSpan
10
10
  import com.swmansion.enriched.spans.EnrichedMentionSpan
11
11
  import com.swmansion.enriched.spans.EnrichedSpans
12
+ import com.swmansion.enriched.utils.EnrichedConstants
12
13
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
14
+ import com.swmansion.enriched.utils.removeZWS
13
15
 
14
- class ParametrizedStyles(private val view: EnrichedTextInputView) {
16
+ class ParametrizedStyles(
17
+ private val view: EnrichedTextInputView,
18
+ ) {
15
19
  private var mentionStart: Int? = null
16
20
  private var isSettingLinkSpan = false
17
21
 
18
22
  var mentionIndicators: Array<String> = emptyArray<String>()
19
23
 
20
- fun <T>removeSpansForRange(spannable: Spannable, start: Int, end: Int, clazz: Class<T>): Boolean {
24
+ fun <T> removeSpansForRange(
25
+ spannable: Spannable,
26
+ start: Int,
27
+ end: Int,
28
+ clazz: Class<T>,
29
+ ): Boolean {
21
30
  val ssb = spannable as SpannableStringBuilder
22
31
  val spans = ssb.getSpans(start, end, clazz)
23
32
  if (spans.isEmpty()) return false
24
33
 
25
- ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
34
+ ssb.removeZWS(start, end)
26
35
 
27
36
  for (span in spans) {
28
37
  ssb.removeSpan(span)
@@ -31,7 +40,12 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
31
40
  return true
32
41
  }
33
42
 
34
- fun setLinkSpan(start: Int, end: Int, text: String, url: String) {
43
+ fun setLinkSpan(
44
+ start: Int,
45
+ end: Int,
46
+ text: String,
47
+ url: String,
48
+ ) {
35
49
  isSettingLinkSpan = true
36
50
 
37
51
  val spannable = view.text as SpannableStringBuilder
@@ -55,36 +69,66 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
55
69
  isSettingLinkSpan = false
56
70
  }
57
71
 
58
- fun afterTextChanged(s: Editable, endCursorPosition: Int) {
59
- val result = getWordAtIndex(s, endCursorPosition) ?: return
60
-
61
- afterTextChangedLinks(result)
62
- afterTextChangedMentions(result)
72
+ fun afterTextChanged(
73
+ s: Editable,
74
+ startCursorPosition: Int,
75
+ endCursorPosition: Int,
76
+ ) {
77
+ afterTextChangedLinks(startCursorPosition, endCursorPosition)
78
+ afterTextChangedMentions(s, startCursorPosition)
63
79
  }
64
80
 
65
- fun detectAllLinks() {
66
- val spannable = view.text as Spannable
67
-
68
- // TODO: Consider using more reliable regex, this one matches almost anything
69
- val urlPattern = android.util.Patterns.WEB_URL.matcher(spannable)
81
+ fun detectLinksInRange(
82
+ spannable: Spannable,
83
+ start: Int,
84
+ end: Int,
85
+ ) {
86
+ val regex = view.linkRegex ?: return
87
+ val contextText = spannable.subSequence(start, end).toString()
70
88
 
71
- val spans = spannable.getSpans(0, spannable.length, EnrichedLinkSpan::class.java)
89
+ val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
72
90
  for (span in spans) {
73
91
  spannable.removeSpan(span)
74
92
  }
75
93
 
76
- while (urlPattern.find()) {
77
- val word = urlPattern.group()
78
- val start = urlPattern.start()
79
- val end = urlPattern.end()
80
- val span = EnrichedLinkSpan(word, view.htmlStyle)
81
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
82
- spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
94
+ val wordsRegex = Regex("\\S+")
95
+ for (wordMatch in wordsRegex.findAll(contextText)) {
96
+ var word = wordMatch.value
97
+ var wordStart = wordMatch.range.first
98
+
99
+ // Do not include zero-width space in link detection
100
+ if (word.startsWith("\u200B")) {
101
+ word = word.substring(1)
102
+ wordStart += 1
103
+ }
104
+
105
+ // Loop over words and detect links
106
+ val matcher = regex.matcher(word)
107
+ while (matcher.find()) {
108
+ val linkStart = matcher.start()
109
+ val linkEnd = matcher.end()
110
+
111
+ val spanStart = start + wordStart + linkStart
112
+ val spanEnd = start + wordStart + linkEnd
113
+
114
+ val span = EnrichedLinkSpan(matcher.group(), view.htmlStyle)
115
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(spanStart, spanEnd)
116
+
117
+ spannable.setSpan(
118
+ span,
119
+ safeStart,
120
+ safeEnd,
121
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,
122
+ )
123
+ }
83
124
  }
84
125
  }
85
126
 
86
- private fun getWordAtIndex(s: CharSequence, index: Int): TextRange? {
87
- if (index < 0 ) return null
127
+ private fun getWordAtIndex(
128
+ s: CharSequence,
129
+ index: Int,
130
+ ): TextRange? {
131
+ if (index < 0) return null
88
132
 
89
133
  var start = index
90
134
  var end = index
@@ -102,8 +146,31 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
102
146
  return TextRange(result, start, end)
103
147
  }
104
148
 
149
+ // After editing text we want to automatically detect links in the affected range
150
+ // Affected range is range + previous word + next word
151
+ private fun getLinksAffectedRange(
152
+ s: CharSequence,
153
+ start: Int,
154
+ end: Int,
155
+ ): IntRange {
156
+ var actualStart = start
157
+ var actualEnd = end
158
+
159
+ // Expand backward to find the start of the first affected word
160
+ while (actualStart > 0 && !Character.isWhitespace(s[actualStart - 1])) {
161
+ actualStart--
162
+ }
163
+
164
+ // Expand forward to find the end of the last affected word
165
+ while (actualEnd < s.length && !Character.isWhitespace(s[actualEnd])) {
166
+ actualEnd++
167
+ }
168
+
169
+ return actualStart..actualEnd
170
+ }
171
+
105
172
  private fun canLinkBeApplied(): Boolean {
106
- val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
173
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle) ?: return true
107
174
  val conflictingStyles = mergingConfig.conflictingStyles
108
175
  val blockingStyles = mergingConfig.blockingStyles
109
176
 
@@ -118,34 +185,29 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
118
185
  return true
119
186
  }
120
187
 
121
- private fun afterTextChangedLinks(result: TextRange) {
188
+ private fun afterTextChangedLinks(
189
+ editStart: Int,
190
+ editEnd: Int,
191
+ ) {
122
192
  // Do not detect link if it's applied manually
123
193
  if (isSettingLinkSpan || !canLinkBeApplied()) return
124
194
 
125
- val spannable = view.text as Spannable
126
- val (word, start, end) = result
127
-
128
- // TODO: Consider using more reliable regex, this one matches almost anything
129
- val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
130
- val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
131
- for (span in spans) {
132
- spannable.removeSpan(span)
133
- }
134
-
135
- if (urlPattern.matches()) {
136
- val span = EnrichedLinkSpan(word, view.htmlStyle)
137
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
138
- spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
139
- }
195
+ val spannable = view.text as? Spannable ?: return
196
+ val affectedRange = getLinksAffectedRange(spannable, editStart, editEnd)
197
+ detectLinksInRange(spannable, affectedRange.first, affectedRange.last)
140
198
  }
141
199
 
142
- private fun afterTextChangedMentions(currentWord: TextRange) {
200
+ private fun afterTextChangedMentions(
201
+ s: CharSequence,
202
+ endCursorPosition: Int,
203
+ ) {
143
204
  val mentionHandler = view.mentionHandler ?: return
205
+ val currentWord = getWordAtIndex(s, endCursorPosition) ?: return
144
206
  val spannable = view.text as Spannable
145
207
 
146
208
  val indicatorsPattern = mentionIndicators.joinToString("|") { Regex.escape(it) }
147
209
  val mentionIndicatorRegex = Regex("^($indicatorsPattern)")
148
- val mentionRegex= Regex("^($indicatorsPattern)\\w*")
210
+ val mentionRegex = Regex("^($indicatorsPattern)\\w*")
149
211
 
150
212
  val spans = spannable.getSpans(currentWord.start, currentWord.end, EnrichedMentionSpan::class.java)
151
213
  for (span in spans) {
@@ -192,20 +254,24 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
192
254
  mentionHandler.onMention(indicator, text)
193
255
  }
194
256
 
195
- fun setImageSpan(src: String, width: Float, height: Float) {
257
+ fun setImageSpan(
258
+ src: String,
259
+ width: Float,
260
+ height: Float,
261
+ ) {
196
262
  if (view.selection == null) return
197
263
  val spannable = view.text as SpannableStringBuilder
198
264
  val (start, originalEnd) = view.selection.getInlineSelection()
199
265
 
200
266
  if (start == originalEnd) {
201
- spannable.insert(start, "\uFFFC")
267
+ spannable.insert(start, EnrichedConstants.ORC_STRING)
202
268
  } else {
203
269
  val spans = spannable.getSpans(start, originalEnd, EnrichedImageSpan::class.java)
204
270
  for (s in spans) {
205
271
  spannable.removeSpan(s)
206
272
  }
207
273
 
208
- spannable.replace(start, originalEnd, "\uFFFC")
274
+ spannable.replace(start, originalEnd, EnrichedConstants.ORC_STRING)
209
275
  }
210
276
 
211
277
  val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1)
@@ -228,7 +294,11 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
228
294
  }
229
295
  }
230
296
 
231
- fun setMentionSpan(indicator: String, text: String, attributes: Map<String, String>) {
297
+ fun setMentionSpan(
298
+ indicator: String,
299
+ text: String,
300
+ attributes: Map<String, String>,
301
+ ) {
232
302
  val selection = view.selection ?: return
233
303
 
234
304
  val spannable = view.text as SpannableStringBuilder
@@ -259,17 +329,23 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
259
329
  view.selection.validateStyles()
260
330
  }
261
331
 
262
- fun getStyleRange(): Pair<Int, Int> {
263
- return view.selection?.getInlineSelection() ?: Pair(0, 0)
264
- }
332
+ fun getStyleRange(): Pair<Int, Int> = view.selection?.getInlineSelection() ?: Pair(0, 0)
265
333
 
266
- fun removeStyle(name: String, start: Int, end: Int): Boolean {
334
+ fun removeStyle(
335
+ name: String,
336
+ start: Int,
337
+ end: Int,
338
+ ): Boolean {
267
339
  val config = EnrichedSpans.parametrizedStyles[name] ?: return false
268
340
  val spannable = view.text as Spannable
269
341
  return removeSpansForRange(spannable, start, end, config.clazz)
270
342
  }
271
343
 
272
344
  companion object {
273
- data class TextRange(val text: String, val start: Int, val end: Int)
345
+ data class TextRange(
346
+ val text: String,
347
+ val start: Int,
348
+ val end: Int,
349
+ )
274
350
  }
275
351
  }
@@ -13,12 +13,12 @@ import android.os.Looper
13
13
  import android.util.Log
14
14
  import androidx.core.content.res.ResourcesCompat
15
15
  import androidx.core.graphics.drawable.DrawableCompat
16
- import java.net.URL
17
- import java.util.concurrent.Executors
18
16
  import androidx.core.graphics.drawable.toDrawable
19
17
  import com.swmansion.enriched.R
18
+ import java.net.URL
19
+ import java.util.concurrent.Executors
20
20
 
21
- class AsyncDrawable (
21
+ class AsyncDrawable(
22
22
  private val url: String,
23
23
  ) : Drawable() {
24
24
  private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable()
@@ -78,11 +78,14 @@ class AsyncDrawable (
78
78
  }
79
79
 
80
80
  @Deprecated("Deprecated in Java")
81
- override fun getOpacity(): Int {
82
- return PixelFormat.TRANSLUCENT
83
- }
81
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
84
82
 
85
- override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
83
+ override fun setBounds(
84
+ left: Int,
85
+ top: Int,
86
+ right: Int,
87
+ bottom: Int,
88
+ ) {
86
89
  super.setBounds(left, top, right, bottom)
87
90
  internalDrawable.setBounds(left, top, right, bottom)
88
91
  }
@@ -0,0 +1,11 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ object EnrichedConstants {
4
+ // Zero Width Space
5
+ const val ZWS = '\u200B'
6
+ const val ZWS_STRING = "\u200B"
7
+
8
+ // Object Replacement Character
9
+ const val ORC = '\uFFFC'
10
+ const val ORC_STRING = "\uFFFC"
11
+ }
@@ -0,0 +1,17 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.text.Editable
4
+ import android.text.Spannable
5
+ import android.text.SpannableStringBuilder
6
+ import com.swmansion.enriched.watchers.EnrichedSpanWatcher
7
+
8
+ class EnrichedEditableFactory(
9
+ private val watcher: EnrichedSpanWatcher,
10
+ ) : Editable.Factory() {
11
+ override fun newEditable(source: CharSequence): Editable {
12
+ val s = source as? SpannableStringBuilder ?: SpannableStringBuilder(source)
13
+ s.removeSpan(watcher)
14
+ s.setSpan(watcher, 0, s.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
15
+ return s
16
+ }
17
+ }