react-native-enriched 0.1.6 → 0.2.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 (68) hide show
  1. package/README.md +3 -9
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +1 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -1
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +0 -45
  5. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +19 -2
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +3 -3
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  8. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
  9. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
  10. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +4 -0
  11. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  12. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
  13. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  14. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
  15. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +57 -30
  16. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  17. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +16 -13
  18. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  19. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -0
  20. package/android/src/main/res/drawable/broken_image.xml +10 -0
  21. package/ios/EnrichedTextInputView.h +3 -0
  22. package/ios/EnrichedTextInputView.mm +97 -29
  23. package/ios/config/InputConfig.h +6 -0
  24. package/ios/config/InputConfig.mm +32 -0
  25. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +0 -45
  26. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
  27. package/ios/inputParser/InputParser.mm +147 -24
  28. package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
  29. package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
  30. package/ios/styles/BlockQuoteStyle.mm +5 -26
  31. package/ios/styles/BoldStyle.mm +2 -0
  32. package/ios/styles/CodeBlockStyle.mm +228 -0
  33. package/ios/styles/H1Style.mm +1 -0
  34. package/ios/styles/H2Style.mm +1 -0
  35. package/ios/styles/H3Style.mm +1 -0
  36. package/ios/styles/ImageStyle.mm +158 -0
  37. package/ios/styles/InlineCodeStyle.mm +2 -0
  38. package/ios/styles/ItalicStyle.mm +2 -0
  39. package/ios/styles/LinkStyle.mm +8 -0
  40. package/ios/styles/MentionStyle.mm +133 -36
  41. package/ios/styles/OrderedListStyle.mm +2 -0
  42. package/ios/styles/StrikethroughStyle.mm +2 -0
  43. package/ios/styles/UnderlineStyle.mm +2 -0
  44. package/ios/styles/UnorderedListStyle.mm +2 -0
  45. package/ios/utils/BaseStyleProtocol.h +1 -0
  46. package/ios/utils/ImageData.h +10 -0
  47. package/ios/utils/ImageData.mm +4 -0
  48. package/ios/utils/LayoutManagerExtension.mm +118 -3
  49. package/ios/utils/OccurenceUtils.h +4 -0
  50. package/ios/utils/OccurenceUtils.mm +47 -0
  51. package/ios/utils/ParagraphAttributesUtils.h +1 -0
  52. package/ios/utils/ParagraphAttributesUtils.mm +87 -20
  53. package/ios/utils/StyleHeaders.h +12 -0
  54. package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
  55. package/lib/module/EnrichedTextInput.js +2 -2
  56. package/lib/module/EnrichedTextInput.js.map +1 -1
  57. package/lib/module/EnrichedTextInputNativeComponent.ts +6 -5
  58. package/lib/module/normalizeHtmlStyle.js +0 -4
  59. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  60. package/lib/typescript/src/EnrichedTextInput.d.ts +1 -5
  61. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  62. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -5
  63. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  64. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/EnrichedTextInput.tsx +3 -7
  67. package/src/EnrichedTextInputNativeComponent.ts +6 -5
  68. package/src/normalizeHtmlStyle.ts +0 -4
@@ -1,6 +1,5 @@
1
1
  package com.swmansion.enriched.styles
2
2
 
3
- import android.net.Uri
4
3
  import android.text.Editable
5
4
  import android.text.Spannable
6
5
  import android.text.SpannableStringBuilder
@@ -11,7 +10,6 @@ import com.swmansion.enriched.spans.EnrichedLinkSpan
11
10
  import com.swmansion.enriched.spans.EnrichedMentionSpan
12
11
  import com.swmansion.enriched.spans.EnrichedSpans
13
12
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
14
- import java.io.File
15
13
 
16
14
  class ParametrizedStyles(private val view: EnrichedTextInputView) {
17
15
  private var mentionStart: Int? = null
@@ -85,7 +83,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
85
83
  }
86
84
  }
87
85
 
88
- private fun getWordAtIndex(s: Editable, index: Int): Triple<String, Int, Int>? {
86
+ private fun getWordAtIndex(s: CharSequence, index: Int): TextRange? {
89
87
  if (index < 0 ) return null
90
88
 
91
89
  var start = index
@@ -101,7 +99,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
101
99
 
102
100
  val result = s.subSequence(start, end).toString()
103
101
 
104
- return Triple(result, start, end)
102
+ return TextRange(result, start, end)
105
103
  }
106
104
 
107
105
  private fun canLinkBeApplied(): Boolean {
@@ -120,7 +118,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
120
118
  return true
121
119
  }
122
120
 
123
- private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
121
+ private fun afterTextChangedLinks(result: TextRange) {
124
122
  // Do not detect link if it's applied manually
125
123
  if (isSettingLinkSpan || !canLinkBeApplied()) return
126
124
 
@@ -141,55 +139,80 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
141
139
  }
142
140
  }
143
141
 
144
- private fun afterTextChangedMentions(result: Triple<String, Int, Int>) {
142
+ private fun afterTextChangedMentions(currentWord: TextRange) {
145
143
  val mentionHandler = view.mentionHandler ?: return
146
144
  val spannable = view.text as Spannable
147
- val (word, start, end) = result
148
145
 
149
146
  val indicatorsPattern = mentionIndicators.joinToString("|") { Regex.escape(it) }
150
147
  val mentionIndicatorRegex = Regex("^($indicatorsPattern)")
151
148
  val mentionRegex= Regex("^($indicatorsPattern)\\w*")
152
149
 
153
- val spans = spannable.getSpans(start, end, EnrichedMentionSpan::class.java)
150
+ val spans = spannable.getSpans(currentWord.start, currentWord.end, EnrichedMentionSpan::class.java)
154
151
  for (span in spans) {
155
152
  spannable.removeSpan(span)
156
153
  }
157
154
 
158
- if (mentionRegex.matches(word)) {
159
- val indicator = mentionIndicatorRegex.find(word)?.value ?: ""
160
- val text = word.replaceFirst(indicator, "")
155
+ var indicator: String
156
+ var finalStart: Int
157
+ val finalEnd = currentWord.end
158
+
159
+ // No mention in the current word, check previous one
160
+ if (!mentionRegex.matches(currentWord.text)) {
161
+ val previousWord = getWordAtIndex(spannable, currentWord.start - 1)
161
162
 
162
- // Means we are starting mention
163
- if (text.isEmpty()) {
164
- mentionStart = start
163
+ // No previous word -> no mention to be detected
164
+ if (previousWord == null) {
165
+ mentionHandler.endMention()
166
+ return
165
167
  }
166
168
 
167
- mentionHandler.onMention(indicator, text)
169
+ // Previous word is not a mention -> end mention
170
+ if (!mentionRegex.matches(previousWord.text)) {
171
+ mentionHandler.endMention()
172
+ return
173
+ }
174
+
175
+ // Previous word is a mention -> use it
176
+ finalStart = previousWord.start
177
+ indicator = mentionIndicatorRegex.find(previousWord.text)?.value ?: ""
168
178
  } else {
169
- mentionHandler.endMention()
179
+ // Current word is a mention -> use it
180
+ finalStart = currentWord.start
181
+ indicator = mentionIndicatorRegex.find(currentWord.text)?.value ?: ""
182
+ }
183
+
184
+ // Extract text without indicator
185
+ val text = spannable.subSequence(finalStart, finalEnd).toString().replaceFirst(indicator, "")
186
+
187
+ // Means we are starting mention
188
+ if (text.isEmpty()) {
189
+ mentionStart = finalStart
170
190
  }
191
+
192
+ mentionHandler.onMention(indicator, text)
171
193
  }
172
194
 
173
- fun setImageSpan(src: String) {
195
+ fun setImageSpan(src: String, width: Float, height: Float) {
174
196
  if (view.selection == null) return
175
-
176
197
  val spannable = view.text as SpannableStringBuilder
177
- var (start, end) = view.selection.getInlineSelection()
178
- val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
198
+ val (start, originalEnd) = view.selection.getInlineSelection()
179
199
 
180
- for (s in spans) {
181
- spannable.removeSpan(s)
182
- }
183
-
184
- if (start == end) {
200
+ if (start == originalEnd) {
185
201
  spannable.insert(start, "\uFFFC")
186
- end++
202
+ } else {
203
+ val spans = spannable.getSpans(start, originalEnd, EnrichedImageSpan::class.java)
204
+ for (s in spans) {
205
+ spannable.removeSpan(s)
206
+ }
207
+
208
+ spannable.replace(start, originalEnd, "\uFFFC")
187
209
  }
188
210
 
189
- val uri = Uri.fromFile(File(src))
190
- val span = EnrichedImageSpan(view.context, uri, view.htmlStyle)
191
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
192
- spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
211
+ val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1)
212
+ val span = EnrichedImageSpan.createEnrichedImageSpan(src, width.toInt(), height.toInt())
213
+ span.observeAsyncDrawableLoaded(view.text)
214
+
215
+ spannable.setSpan(span, imageStart, imageEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
193
216
  }
194
217
 
195
218
  fun startMention(indicator: String) {
@@ -245,4 +268,8 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
245
268
  val spannable = view.text as Spannable
246
269
  return removeSpansForRange(spannable, start, end, config.clazz)
247
270
  }
271
+
272
+ companion object {
273
+ data class TextRange(val text: String, val start: Int, val end: Int)
274
+ }
248
275
  }
@@ -0,0 +1,91 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.res.Resources
5
+ import android.graphics.BitmapFactory
6
+ import android.graphics.Canvas
7
+ import android.graphics.Color
8
+ import android.graphics.ColorFilter
9
+ import android.graphics.PixelFormat
10
+ import android.graphics.drawable.Drawable
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.util.Log
14
+ import androidx.core.content.res.ResourcesCompat
15
+ import androidx.core.graphics.drawable.DrawableCompat
16
+ import java.net.URL
17
+ import java.util.concurrent.Executors
18
+ import androidx.core.graphics.drawable.toDrawable
19
+ import com.swmansion.enriched.R
20
+
21
+ class AsyncDrawable (
22
+ private val url: String,
23
+ ) : Drawable() {
24
+ private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable()
25
+ private val mainHandler = Handler(Looper.getMainLooper())
26
+ private val executor = Executors.newSingleThreadExecutor()
27
+ var isLoaded = false
28
+
29
+ init {
30
+ internalDrawable.bounds = bounds
31
+
32
+ load()
33
+ }
34
+
35
+ private fun load() {
36
+ executor.execute {
37
+ try {
38
+ isLoaded = false
39
+ val inputStream = URL(url).openStream()
40
+ val bitmap = BitmapFactory.decodeStream(inputStream)
41
+
42
+ // Switch to Main Thread to update UI
43
+ mainHandler.post {
44
+ if (bitmap != null) {
45
+ val d = bitmap.toDrawable(Resources.getSystem())
46
+
47
+ d.bounds = bounds
48
+ internalDrawable = d
49
+ } else {
50
+ loadPlaceholderImage()
51
+ }
52
+ }
53
+ } catch (e: Exception) {
54
+ Log.e("AsyncDrawable", "Failed to load: $url", e)
55
+
56
+ loadPlaceholderImage()
57
+ } finally {
58
+ isLoaded = true
59
+ onLoaded?.invoke()
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun loadPlaceholderImage() {
65
+ internalDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
66
+ }
67
+
68
+ override fun draw(canvas: Canvas) {
69
+ internalDrawable.draw(canvas)
70
+ }
71
+
72
+ override fun setAlpha(alpha: Int) {
73
+ internalDrawable.alpha = alpha
74
+ }
75
+
76
+ override fun setColorFilter(colorFilter: ColorFilter?) {
77
+ internalDrawable.colorFilter = colorFilter
78
+ }
79
+
80
+ @Deprecated("Deprecated in Java")
81
+ override fun getOpacity(): Int {
82
+ return PixelFormat.TRANSLUCENT
83
+ }
84
+
85
+ override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
86
+ super.setBounds(left, top, right, bottom)
87
+ internalDrawable.setBounds(left, top, right, bottom)
88
+ }
89
+
90
+ var onLoaded: (() -> Unit)? = null
91
+ }
@@ -275,7 +275,16 @@ public class EnrichedParser {
275
275
  if (style[j] instanceof EnrichedImageSpan) {
276
276
  out.append("<img src=\"");
277
277
  out.append(((EnrichedImageSpan) style[j]).getSource());
278
- out.append("\">");
278
+ out.append("\"");
279
+
280
+ out.append(" width=\"");
281
+ out.append(((EnrichedImageSpan) style[j]).getWidth());
282
+ out.append("\"");
283
+
284
+ out.append(" height=\"");
285
+ out.append(((EnrichedImageSpan) style[j]).getHeight());
286
+
287
+ out.append("\"/>");
279
288
  // Don't output the placeholder character underlying the image.
280
289
  i = next;
281
290
  }
@@ -412,7 +421,6 @@ class HtmlToSpannedConverter implements ContentHandler {
412
421
  }
413
422
 
414
423
  private void handleStartTag(String tag, Attributes attributes) {
415
- isEmptyTag = false;
416
424
  if (tag.equalsIgnoreCase("br")) {
417
425
  // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
418
426
  // so we can safely emit the linebreaks when we handle the close tag.
@@ -454,7 +462,7 @@ class HtmlToSpannedConverter implements ContentHandler {
454
462
  } else if (tag.equalsIgnoreCase("h3")) {
455
463
  startHeading(mSpannableStringBuilder, 3);
456
464
  } else if (tag.equalsIgnoreCase("img")) {
457
- startImg(mSpannableStringBuilder, attributes, mImageGetter, mStyle);
465
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
458
466
  } else if (tag.equalsIgnoreCase("code")) {
459
467
  start(mSpannableStringBuilder, new Code());
460
468
  } else if (tag.equalsIgnoreCase("mention")) {
@@ -679,20 +687,15 @@ class HtmlToSpannedConverter implements ContentHandler {
679
687
  }
680
688
  }
681
689
 
682
- private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img, HtmlStyle style) {
690
+ private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
683
691
  String src = attributes.getValue("", "src");
684
- Drawable d = null;
685
- if (img != null) {
686
- d = img.getDrawable(src);
687
- }
688
-
689
- if (d == null) {
690
- return;
691
- }
692
+ String width = attributes.getValue("", "width");
693
+ String height = attributes.getValue("", "height");
692
694
 
693
695
  int len = text.length();
696
+ EnrichedImageSpan span = EnrichedImageSpan.Companion.createEnrichedImageSpan(src, Integer.parseInt(width), Integer.parseInt(height));
694
697
  text.append("");
695
- text.setSpan(new EnrichedImageSpan(d, src, style), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
698
+ text.setSpan(span, len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
696
699
  }
697
700
 
698
701
  private static void startA(Editable text, Attributes attributes) {
@@ -0,0 +1,26 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.Resources
6
+ import android.graphics.drawable.Drawable
7
+ import androidx.core.content.res.ResourcesCompat
8
+ import androidx.core.graphics.drawable.DrawableCompat
9
+
10
+ object ResourceManager {
11
+ private var appContext: Context? = null
12
+
13
+ fun init(context: Context) {
14
+ this.appContext = context.applicationContext
15
+ }
16
+
17
+ @SuppressLint("UseCompatLoadingForDrawables")
18
+ fun getDrawableResource(id: Int): Drawable {
19
+ val context = appContext ?: throw IllegalStateException("ResourceManager not initialized! Call init() first.")
20
+
21
+ val image = ResourcesCompat.getDrawable(context.resources, id, null)
22
+ val finalImage = image ?: Resources.getSystem().getDrawable(android.R.drawable.ic_menu_report_image)
23
+
24
+ return DrawableCompat.wrap(finalImage)
25
+ }
26
+ }
@@ -52,6 +52,9 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
52
52
  }
53
53
 
54
54
  fun emitEvent(s: Spannable, what: Any?) {
55
+ // Do not parse spannable and emit event if onChangeHtml is not provided
56
+ if (!view.shouldEmitHtml) return
57
+
55
58
  // Emit event only if we change one of ours spans
56
59
  if (what != null && what !is EnrichedSpan) return
57
60
 
@@ -0,0 +1,10 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="960"
5
+ android:viewportHeight="960"
6
+ android:tint="?attr/colorControlNormal">
7
+ <path
8
+ android:fillColor="@android:color/white"
9
+ android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z"/>
10
+ </vector>
@@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
18
18
  @public InputParser *parser;
19
19
  @public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
20
20
  @public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
21
+ NSDictionary<NSNumber *, NSArray<NSNumber *> *> *conflictingStyles;
22
+ NSDictionary<NSNumber *, NSArray<NSNumber *> *> *blockingStyles;
21
23
  @public BOOL blockEmitting;
22
24
  }
23
25
  - (CGSize)measureSize:(CGFloat)maxWidth;
@@ -25,6 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
25
27
  - (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
26
28
  - (void)anyTextMayHaveBeenModified;
27
29
  - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
30
+ - (NSArray<NSNumber *> *)getPresentStyleTypesFrom:(NSArray<NSNumber *> *)types range:(NSRange)range;
28
31
  @end
29
32
 
30
33
  NS_ASSUME_NONNULL_END
@@ -26,8 +26,6 @@ using namespace facebook::react;
26
26
  EnrichedTextInputViewShadowNode::ConcreteState::Shared _state;
27
27
  int _componentViewHeightUpdateCounter;
28
28
  NSMutableSet<NSNumber *> *_activeStyles;
29
- NSDictionary<NSNumber *, NSArray<NSNumber *> *> *_conflictingStyles;
30
- NSDictionary<NSNumber *, NSArray<NSNumber *> *> *_blockingStyles;
31
29
  LinkData *_recentlyActiveLinkData;
32
30
  NSRange _recentlyActiveLinkRange;
33
31
  NSString *_recentlyEmittedString;
@@ -95,10 +93,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
95
93
  @([H3Style getStyleType]): [[H3Style alloc] initWithInput:self],
96
94
  @([UnorderedListStyle getStyleType]): [[UnorderedListStyle alloc] initWithInput:self],
97
95
  @([OrderedListStyle getStyleType]): [[OrderedListStyle alloc] initWithInput:self],
98
- @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self]
96
+ @([BlockQuoteStyle getStyleType]): [[BlockQuoteStyle alloc] initWithInput:self],
97
+ @([CodeBlockStyle getStyleType]): [[CodeBlockStyle alloc] initWithInput:self],
98
+ @([ImageStyle getStyleType]): [[ImageStyle alloc] initWithInput:self]
99
99
  };
100
100
 
101
- _conflictingStyles = @{
101
+ conflictingStyles = @{
102
102
  @([BoldStyle getStyleType]) : @[],
103
103
  @([ItalicStyle getStyleType]) : @[],
104
104
  @([UnderlineStyle getStyleType]) : @[],
@@ -106,28 +106,33 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
106
106
  @([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
107
107
  @([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
108
108
  @([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])],
109
- @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])],
110
- @([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])],
111
- @([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])],
112
- @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])],
113
- @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType])],
114
- @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType])]
109
+ @([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])],
110
+ @([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])],
111
+ @([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])],
112
+ @([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])],
113
+ @([OrderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([CodeBlockStyle getStyleType])],
114
+ @([BlockQuoteStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([CodeBlockStyle getStyleType])],
115
+ @([CodeBlockStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType]),
116
+ @([BoldStyle getStyleType]), @([ItalicStyle getStyleType]), @([UnderlineStyle getStyleType]), @([StrikethroughStyle getStyleType]), @([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]), @([BlockQuoteStyle getStyleType]), @([InlineCodeStyle getStyleType]), @([MentionStyle getStyleType]), @([LinkStyle getStyleType])],
117
+ @([ImageStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])]
115
118
  };
116
119
 
117
- _blockingStyles = @{
118
- @([BoldStyle getStyleType]) : @[],
119
- @([ItalicStyle getStyleType]) : @[],
120
- @([UnderlineStyle getStyleType]) : @[],
121
- @([StrikethroughStyle getStyleType]) : @[],
122
- @([InlineCodeStyle getStyleType]) : @[],
123
- @([LinkStyle getStyleType]): @[],
124
- @([MentionStyle getStyleType]): @[],
120
+ blockingStyles = @{
121
+ @([BoldStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
122
+ @([ItalicStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
123
+ @([UnderlineStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
124
+ @([StrikethroughStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
125
+ @([InlineCodeStyle getStyleType]) : @[@([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType])],
126
+ @([LinkStyle getStyleType]): @[@([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType])],
127
+ @([MentionStyle getStyleType]): @[@([CodeBlockStyle getStyleType]), @([ImageStyle getStyleType])],
125
128
  @([H1Style getStyleType]): @[],
126
129
  @([H2Style getStyleType]): @[],
127
130
  @([H3Style getStyleType]): @[],
128
131
  @([UnorderedListStyle getStyleType]): @[],
129
132
  @([OrderedListStyle getStyleType]): @[],
130
133
  @([BlockQuoteStyle getStyleType]): @[],
134
+ @([CodeBlockStyle getStyleType]): @[],
135
+ @([ImageStyle getStyleType]) : @[@([InlineCodeStyle getStyleType])]
131
136
  };
132
137
 
133
138
  parser = [[InputParser alloc] initWithInput:self];
@@ -347,6 +352,25 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
347
352
  }
348
353
  }
349
354
 
355
+ if(newViewProps.htmlStyle.codeblock.color != oldViewProps.htmlStyle.codeblock.color) {
356
+ if(isColorMeaningful(newViewProps.htmlStyle.codeblock.color)) {
357
+ [newConfig setCodeBlockFgColor:RCTUIColorFromSharedColor(newViewProps.htmlStyle.codeblock.color)];
358
+ stylePropChanged = YES;
359
+ }
360
+ }
361
+
362
+ if(newViewProps.htmlStyle.codeblock.backgroundColor != oldViewProps.htmlStyle.codeblock.backgroundColor) {
363
+ if(isColorMeaningful(newViewProps.htmlStyle.codeblock.backgroundColor)) {
364
+ [newConfig setCodeBlockBgColor:RCTUIColorFromSharedColor(newViewProps.htmlStyle.codeblock.backgroundColor)];
365
+ stylePropChanged = YES;
366
+ }
367
+ }
368
+
369
+ if(newViewProps.htmlStyle.codeblock.borderRadius != oldViewProps.htmlStyle.codeblock.borderRadius) {
370
+ [newConfig setCodeBlockBorderRadius:newViewProps.htmlStyle.codeblock.borderRadius];
371
+ stylePropChanged = YES;
372
+ }
373
+
350
374
  if(newViewProps.htmlStyle.a.textDecorationLine != oldViewProps.htmlStyle.a.textDecorationLine) {
351
375
  NSString *objcString = [NSString fromCppString:newViewProps.htmlStyle.a.textDecorationLine];
352
376
  if([objcString isEqualToString:DecorationUnderline]) {
@@ -512,9 +536,8 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
512
536
  _emitHtml = newViewProps.isOnChangeHtmlSet;
513
537
 
514
538
  [super updateProps:props oldProps:oldProps];
515
- // mandatory text and height checks
539
+ // run the changes callback
516
540
  [self anyTextMayHaveBeenModified];
517
- [self tryUpdatingHeight];
518
541
 
519
542
  // autofocus - needs to be done at the very end
520
543
  if(isFirstMount && newViewProps.autoFocus) {
@@ -575,7 +598,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
575
598
  options: NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
576
599
  context: nullptr
577
600
  ];
578
-
601
+
579
602
  return CGSizeMake(maxWidth, ceil(boundingBox.size.height));
580
603
  }
581
604
 
@@ -702,8 +725,8 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
702
725
  .isUnorderedList = [_activeStyles containsObject: @([UnorderedListStyle getStyleType])],
703
726
  .isOrderedList = [_activeStyles containsObject: @([OrderedListStyle getStyleType])],
704
727
  .isBlockQuote = [_activeStyles containsObject: @([BlockQuoteStyle getStyleType])],
705
- .isCodeBlock = NO, // [_activeStyles containsObject: @([CodeBlockStyle getStyleType])],
706
- .isImage = NO // [_activeStyles containsObject: @([ImageStyle getStyleType]])],
728
+ .isCodeBlock = [_activeStyles containsObject: @([CodeBlockStyle getStyleType])],
729
+ .isImage = [_activeStyles containsObject: @([ImageStyle getStyleType])],
707
730
  });
708
731
  }
709
732
  }
@@ -771,6 +794,14 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
771
794
  [self toggleParagraphStyle:[OrderedListStyle getStyleType]];
772
795
  } else if([commandName isEqualToString:@"toggleBlockQuote"]) {
773
796
  [self toggleParagraphStyle:[BlockQuoteStyle getStyleType]];
797
+ } else if([commandName isEqualToString:@"toggleCodeBlock"]) {
798
+ [self toggleParagraphStyle:[CodeBlockStyle getStyleType]];
799
+ } else if([commandName isEqualToString:@"addImage"]) {
800
+ NSString *uri = (NSString *)args[0];
801
+ CGFloat imgWidth = [(NSNumber*)args[1] floatValue];
802
+ CGFloat imgHeight = [(NSNumber*)args[2] floatValue];
803
+
804
+ [self addImage:uri width:imgWidth height:imgHeight];
774
805
  }
775
806
  }
776
807
 
@@ -917,11 +948,22 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
917
948
  }
918
949
  }
919
950
 
951
+ - (void)addImage:(NSString *)uri width:(float)width height:(float)height
952
+ {
953
+ ImageStyle *imageStyleClass = (ImageStyle *)stylesDict[@([ImageStyle getStyleType])];
954
+ if(imageStyleClass == nullptr) { return; }
955
+
956
+ if([self handleStyleBlocksAndConflicts:[ImageStyle getStyleType] range:textView.selectedRange]) {
957
+ [imageStyleClass addImage:uri width:width height:height];
958
+ [self anyTextMayHaveBeenModified];
959
+ }
960
+ }
961
+
920
962
  - (void)startMentionWithIndicator:(NSString *)indicator {
921
963
  MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
922
964
  if(mentionStyleClass == nullptr) { return; }
923
965
 
924
- if([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] range:[[mentionStyleClass getActiveMentionRange] rangeValue]]) {
966
+ if([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] range:textView.selectedRange]) {
925
967
  [mentionStyleClass startMentionWithIndicator:indicator];
926
968
  [self anyTextMayHaveBeenModified];
927
969
  }
@@ -930,13 +972,13 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
930
972
  // returns false when style shouldn't be applied and true when it can be
931
973
  - (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range {
932
974
  // handle blocking styles: if any is present we do not apply the toggled style
933
- NSArray<NSNumber *> *blocking = [self getPresentStyleTypesFrom: _blockingStyles[@(type)] range:range];
975
+ NSArray<NSNumber *> *blocking = [self getPresentStyleTypesFrom: blockingStyles[@(type)] range:range];
934
976
  if(blocking.count != 0) {
935
977
  return NO;
936
978
  }
937
979
 
938
980
  // handle conflicting styles: all of their occurences have to be removed
939
- NSArray<NSNumber *> *conflicting = [self getPresentStyleTypesFrom: _conflictingStyles[@(type)] range:range];
981
+ NSArray<NSNumber *> *conflicting = [self getPresentStyleTypesFrom: conflictingStyles[@(type)] range:range];
940
982
  if(conflicting.count != 0) {
941
983
  for(NSNumber *style in conflicting) {
942
984
  id<BaseStyleProtocol> styleClass = stylesDict[style];
@@ -986,7 +1028,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
986
1028
  MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
987
1029
  if(mentionStyleClass != nullptr) {
988
1030
  [mentionStyleClass manageMentionTypingAttributes];
989
- [mentionStyleClass manageMentionEditing];
1031
+
1032
+ // mention editing runs if only a selection was done (no text change)
1033
+ // otherwise we would double-emit with a second call in the anyTextMayHaveBeenModified method
1034
+ if([_recentlyEmittedString isEqualToString:[textView.textStorage.string copy]]) {
1035
+ [mentionStyleClass manageMentionEditing];
1036
+ }
990
1037
  }
991
1038
 
992
1039
  // typing attributes for empty lines selection reset
@@ -1012,6 +1059,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1012
1059
  - (void)handleWordModificationBasedChanges:(NSString*)word inRange:(NSRange)range {
1013
1060
  // manual links refreshing and automatic links detection handling
1014
1061
  LinkStyle* linkStyle = [stylesDict objectForKey:@([LinkStyle getStyleType])];
1062
+
1015
1063
  if(linkStyle != nullptr) {
1016
1064
  // manual links need to be handled first because they can block automatic links after being refreshed
1017
1065
  [linkStyle handleManualLinks:word inRange:range];
@@ -1046,6 +1094,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1046
1094
  [bqStyle manageBlockquoteColor];
1047
1095
  }
1048
1096
 
1097
+ // codeblock font and color management
1098
+ CodeBlockStyle *codeBlockStyle = stylesDict[@([CodeBlockStyle getStyleType])];
1099
+ if(codeBlockStyle != nullptr) {
1100
+ [codeBlockStyle manageCodeBlockFontAndColor];
1101
+ }
1102
+
1049
1103
  // improper headings fix
1050
1104
  H1Style *h1Style = stylesDict[@([H1Style getStyleType])];
1051
1105
  H2Style *h2Style = stylesDict[@([H2Style getStyleType])];
@@ -1056,10 +1110,11 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1056
1110
  [h3Style handleImproperHeadings];
1057
1111
  }
1058
1112
 
1059
- // mentions removal management
1113
+ // mentions management: removal and editing
1060
1114
  MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
1061
1115
  if(mentionStyleClass != nullptr) {
1062
1116
  [mentionStyleClass handleExistingMentions];
1117
+ [mentionStyleClass manageMentionEditing];
1063
1118
  }
1064
1119
 
1065
1120
  // placholder management
@@ -1127,6 +1182,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1127
1182
  [self->textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
1128
1183
  [self->textView.layoutManager ensureLayoutForCharacterRange:actualRange];
1129
1184
  [self->textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
1185
+
1186
+ // We have to explicitly set contentSize
1187
+ // That way textView knows if content overflows and if should be scrollable
1188
+ // We recall measureSize here because value returned from previous measureSize may not be up-to date at that point
1189
+ CGSize measuredSize = [self measureSize:self->textView.frame.size.width];
1190
+ self->textView.contentSize = measuredSize;
1130
1191
  });
1131
1192
  }
1132
1193
 
@@ -1171,6 +1232,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1171
1232
  UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])];
1172
1233
  OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])];
1173
1234
  BlockQuoteStyle *bqStyle = stylesDict[@([BlockQuoteStyle getStyleType])];
1235
+ CodeBlockStyle *cbStyle = stylesDict[@([CodeBlockStyle getStyleType])];
1174
1236
  LinkStyle *linkStyle = stylesDict[@([LinkStyle getStyleType])];
1175
1237
  MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])];
1176
1238
  H1Style *h1Style = stylesDict[@([H1Style getStyleType])];
@@ -1186,13 +1248,19 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
1186
1248
  [oStyle handleBackspaceInRange:range replacementText:text] ||
1187
1249
  [oStyle tryHandlingListShorcutInRange:range replacementText:text] ||
1188
1250
  [bqStyle handleBackspaceInRange:range replacementText:text] ||
1251
+ [cbStyle handleBackspaceInRange:range replacementText:text] ||
1189
1252
  [linkStyle handleLeadingLinkReplacement:range replacementText:text] ||
1190
1253
  [mentionStyle handleLeadingMentionReplacement:range replacementText:text] ||
1191
1254
  [h1Style handleNewlinesInRange:range replacementText:text] ||
1192
1255
  [h2Style handleNewlinesInRange:range replacementText:text] ||
1193
1256
  [h3Style handleNewlinesInRange:range replacementText:text] ||
1194
1257
  [ZeroWidthSpaceUtils handleBackspaceInRange:range replacementText:text input:self] ||
1195
- [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text input:self]
1258
+ [ParagraphAttributesUtils handleBackspaceInRange:range replacementText:text input:self] ||
1259
+ // CRITICAL: This callback HAS TO be always evaluated last.
1260
+ //
1261
+ // This function is the "Generic Fallback": if no specific style claims the backspace action
1262
+ // to change its state, only then do we proceed to physically delete the newline and merge paragraphs.
1263
+ [ParagraphAttributesUtils handleNewlineBackspaceInRange:range replacementText:text input:self]
1196
1264
  ) {
1197
1265
  [self anyTextMayHaveBeenModified];
1198
1266
  return NO;
@@ -64,4 +64,10 @@
64
64
  - (void)setLinkDecorationLine:(TextDecorationLineEnum)newValue;
65
65
  - (void)setMentionStyleProps:(NSDictionary *)newValue;
66
66
  - (MentionStyleProps *)mentionStylePropsForIndicator:(NSString *)indicator;
67
+ - (UIColor *)codeBlockFgColor;
68
+ - (void)setCodeBlockFgColor:(UIColor *)newValue;
69
+ - (UIColor *)codeBlockBgColor;
70
+ - (void)setCodeBlockBgColor:(UIColor *)newValue;
71
+ - (CGFloat)codeBlockBorderRadius;
72
+ - (void)setCodeBlockBorderRadius:(CGFloat)newValue;
67
73
  @end