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
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <img src="https://github.com/user-attachments/assets/abc75d3b-495b-4a76-a72f-d87ce3ca1ff9" alt="react-native-enriched by Software Mansion" width="100%">
1
+ <img src="https://github.com/user-attachments/assets/b010571e-e4a3-4d92-a409-4f9fe614025d" alt="react-native-enriched by Software Mansion" width="100%">
2
2
 
3
3
  # react-native-enriched
4
4
 
@@ -101,11 +101,11 @@ export default function App() {
101
101
  <View style={styles.container}>
102
102
  <EnrichedTextInput
103
103
  ref={ref}
104
- onChangeState={(e) => setStylesState(e.nativeEvent)}
104
+ onChangeState={e => setStylesState(e.nativeEvent)}
105
105
  style={styles.input}
106
106
  />
107
107
  <Button
108
- title="Toggle bold"
108
+ title={stylesState?.isBold ? 'Unbold' : 'Bold'}
109
109
  color={stylesState?.isBold ? 'green' : 'gray'}
110
110
  onPress={() => ref.current?.toggleBold()}
111
111
  />
@@ -152,9 +152,6 @@ Supported styles:
152
152
  - ordered list
153
153
  - unordered list
154
154
 
155
- > [!NOTE]
156
- > The iOS doesn't support codeblocks just yet, but it's planned in the near future!
157
-
158
155
  Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
159
156
 
160
157
  Each call toggles the style within the current text selection. We can still divide styles into two categories based on how they treat the selection:
@@ -212,9 +209,6 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se
212
209
 
213
210
  The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
214
211
 
215
- > [!NOTE]
216
- > The iOS doesn't support inline images just yet, but it's planned in the near future!
217
-
218
212
  ## Style Detection
219
213
 
220
214
  All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload.
@@ -136,7 +136,7 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
136
136
  mViewManager.addLink(view, args.getInt(0), args.getInt(1), args.getString(2), args.getString(3));
137
137
  break;
138
138
  case "addImage":
139
- mViewManager.addImage(view, args.getString(0));
139
+ mViewManager.addImage(view, args.getString(0), (float) args.getDouble(1), (float) args.getDouble(2));
140
140
  break;
141
141
  case "startMention":
142
142
  mViewManager.startMention(view, args.getString(0));
@@ -50,7 +50,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
50
50
  void toggleOrderedList(T view);
51
51
  void toggleUnorderedList(T view);
52
52
  void addLink(T view, int start, int end, String text, String url);
53
- void addImage(T view, String uri);
53
+ void addImage(T view, String uri, float width, float height);
54
54
  void startMention(T view, String indicator);
55
55
  void addMention(T view, String indicator, String text, String payload);
56
56
  }
@@ -309,45 +309,6 @@ static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleAStru
309
309
  }
310
310
  #endif
311
311
 
312
- struct EnrichedTextInputViewHtmlStyleImgStruct {
313
- Float width{0.0};
314
- Float height{0.0};
315
-
316
- #ifdef RN_SERIALIZABLE_STATE
317
- bool operator==(const EnrichedTextInputViewHtmlStyleImgStruct&) const = default;
318
-
319
- folly::dynamic toDynamic() const {
320
- folly::dynamic result = folly::dynamic::object();
321
- result["width"] = width;
322
- result["height"] = height;
323
- return result;
324
- }
325
- #endif
326
- };
327
-
328
- static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, EnrichedTextInputViewHtmlStyleImgStruct &result) {
329
- auto map = (std::unordered_map<std::string, RawValue>)value;
330
-
331
- auto tmp_width = map.find("width");
332
- if (tmp_width != map.end()) {
333
- fromRawValue(context, tmp_width->second, result.width);
334
- }
335
- auto tmp_height = map.find("height");
336
- if (tmp_height != map.end()) {
337
- fromRawValue(context, tmp_height->second, result.height);
338
- }
339
- }
340
-
341
- static inline std::string toString(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
342
- return "[Object EnrichedTextInputViewHtmlStyleImgStruct]";
343
- }
344
-
345
- #ifdef RN_SERIALIZABLE_STATE
346
- static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
347
- return value.toDynamic();
348
- }
349
- #endif
350
-
351
312
  struct EnrichedTextInputViewHtmlStyleOlStruct {
352
313
  Float gapWidth{0.0};
353
314
  Float marginLeft{0.0};
@@ -459,7 +420,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
459
420
  EnrichedTextInputViewHtmlStyleCodeStruct code{};
460
421
  EnrichedTextInputViewHtmlStyleAStruct a{};
461
422
  folly::dynamic mention{};
462
- EnrichedTextInputViewHtmlStyleImgStruct img{};
463
423
  EnrichedTextInputViewHtmlStyleOlStruct ol{};
464
424
  EnrichedTextInputViewHtmlStyleUlStruct ul{};
465
425
 
@@ -476,7 +436,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
476
436
  result["code"] = ::facebook::react::toDynamic(code);
477
437
  result["a"] = ::facebook::react::toDynamic(a);
478
438
  result["mention"] = mention;
479
- result["img"] = ::facebook::react::toDynamic(img);
480
439
  result["ol"] = ::facebook::react::toDynamic(ol);
481
440
  result["ul"] = ::facebook::react::toDynamic(ul);
482
441
  return result;
@@ -519,10 +478,6 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
519
478
  if (tmp_mention != map.end()) {
520
479
  fromRawValue(context, tmp_mention->second, result.mention);
521
480
  }
522
- auto tmp_img = map.find("img");
523
- if (tmp_img != map.end()) {
524
- fromRawValue(context, tmp_img->second, result.img);
525
- }
526
481
  auto tmp_ol = map.find("ol");
527
482
  if (tmp_ol != map.end()) {
528
483
  fromRawValue(context, tmp_ol->second, result.ol);
@@ -29,6 +29,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
29
29
  import com.swmansion.enriched.events.MentionHandler
30
30
  import com.swmansion.enriched.events.OnInputBlurEvent
31
31
  import com.swmansion.enriched.events.OnInputFocusEvent
32
+ import com.swmansion.enriched.spans.EnrichedImageSpan
32
33
  import com.swmansion.enriched.spans.EnrichedSpans
33
34
  import com.swmansion.enriched.styles.InlineStyles
34
35
  import com.swmansion.enriched.styles.ListStyles
@@ -61,6 +62,7 @@ class EnrichedTextInputView : AppCompatEditText {
61
62
  var spanWatcher: EnrichedSpanWatcher? = null
62
63
  var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
63
64
 
65
+ var shouldEmitHtml: Boolean = true
64
66
  var experimentalSynchronousEvents: Boolean = false
65
67
 
66
68
  var fontSize: Float? = null
@@ -254,6 +256,7 @@ class EnrichedTextInputView : AppCompatEditText {
254
256
  val newText = parseText(value)
255
257
  setText(newText)
256
258
 
259
+ observeAsyncImages()
257
260
  // Assign SpanWatcher one more time as our previous spannable has been replaced
258
261
  addSpanWatcher(EnrichedSpanWatcher(this))
259
262
 
@@ -262,6 +265,20 @@ class EnrichedTextInputView : AppCompatEditText {
262
265
  }
263
266
  }
264
267
 
268
+ /**
269
+ * Finds all async images in the current text and sets up listeners
270
+ * to redraw the text layout when they finish downloading.
271
+ */
272
+ private fun observeAsyncImages() {
273
+ val liveText = text ?: return
274
+
275
+ val spans = liveText.getSpans(0, liveText.length, EnrichedImageSpan::class.java)
276
+
277
+ for (span in spans) {
278
+ span.observeAsyncDrawableLoaded(liveText)
279
+ }
280
+ }
281
+
265
282
  fun setAutoFocus(autoFocus: Boolean) {
266
283
  this.autoFocus = autoFocus
267
284
  }
@@ -527,11 +544,11 @@ class EnrichedTextInputView : AppCompatEditText {
527
544
  parametrizedStyles?.setLinkSpan(start, end, text, url)
528
545
  }
529
546
 
530
- fun addImage(src: String) {
547
+ fun addImage(src: String, width: Float, height: Float) {
531
548
  val isValid = verifyStyle(EnrichedSpans.IMAGE)
532
549
  if (!isValid) return
533
550
 
534
- parametrizedStyles?.setImageSpan(src)
551
+ parametrizedStyles?.setImageSpan(src, width, height)
535
552
  layoutManager.invalidateLayout()
536
553
  }
537
554
 
@@ -177,7 +177,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
177
177
  }
178
178
 
179
179
  override fun setIsOnChangeHtmlSet(view: EnrichedTextInputView?, value: Boolean) {
180
- // this prop isn't used on Android as of now, but the setter must be present
180
+ view?.shouldEmitHtml = value
181
181
  }
182
182
 
183
183
  override fun setAutoCapitalize(view: EnrichedTextInputView?, flag: String?) {
@@ -255,8 +255,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
255
255
  view?.addLink(start, end, text, url)
256
256
  }
257
257
 
258
- override fun addImage(view: EnrichedTextInputView?, src: String) {
259
- view?.addImage(src)
258
+ override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
259
+ view?.addImage(src, width, height)
260
260
  }
261
261
 
262
262
  override fun startMention(view: EnrichedTextInputView?, indicator: String) {
@@ -4,10 +4,12 @@ import com.facebook.react.ReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.uimanager.ViewManager
7
+ import com.swmansion.enriched.utils.ResourceManager
7
8
  import java.util.ArrayList
8
9
 
9
10
  class EnrichedTextInputViewPackage : ReactPackage {
10
11
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
12
+ ResourceManager.init(reactContext.applicationContext)
11
13
  val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
12
14
  viewManagers.add(EnrichedTextInputViewManager())
13
15
  return viewManagers
@@ -2,8 +2,10 @@ package com.swmansion.enriched.spans
2
2
 
3
3
  import android.graphics.Canvas
4
4
  import android.graphics.Paint
5
+ import android.graphics.Path
5
6
  import android.graphics.RectF
6
7
  import android.graphics.Typeface
8
+ import android.text.Spanned
7
9
  import android.text.TextPaint
8
10
  import android.text.style.LineBackgroundSpan
9
11
  import android.text.style.MetricAffectingSpan
@@ -33,10 +35,43 @@ class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingS
33
35
  end: Int,
34
36
  lineNum: Int
35
37
  ) {
38
+ if (text !is Spanned) {
39
+ return
40
+ }
41
+
36
42
  val previousColor = p.color
37
43
  p.color = htmlStyle.codeBlockBackgroundColor
44
+
45
+ val radius = htmlStyle.codeBlockRadius
46
+
47
+ val spanStart = text.getSpanStart(this)
48
+ val spanEnd = text.getSpanEnd(this)
49
+ val isFirstLineOfSpan = start == spanStart
50
+ val isLastLineOfSpan = end == spanEnd || (spanEnd + 1 == end && text[spanEnd] == '\n')
51
+
52
+ val path = Path()
53
+ val radii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
54
+
55
+ if (isFirstLineOfSpan) {
56
+ // Top-Left and Top-Right corners
57
+ radii[0] = radius
58
+ radii[1] = radius
59
+ radii[2] = radius
60
+ radii[3] = radius
61
+ }
62
+
63
+ if (isLastLineOfSpan) {
64
+ // Bottom-Right and Bottom-Left corners
65
+ radii[4] = radius
66
+ radii[5] = radius
67
+ radii[6] = radius
68
+ radii[7] = radius
69
+ }
70
+
38
71
  val rect = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
39
- canvas.drawRoundRect(rect, htmlStyle.codeBlockRadius, htmlStyle.codeBlockRadius, p)
72
+
73
+ path.addRoundRect(rect, radii, Path.Direction.CW)
74
+ canvas.drawPath(path, p)
40
75
  p.color = previousColor
41
76
  }
42
77
  }
@@ -1,24 +1,32 @@
1
1
  package com.swmansion.enriched.spans
2
2
 
3
- import android.content.Context
3
+ import android.annotation.SuppressLint
4
+ import android.content.res.Resources
5
+ import android.graphics.BitmapFactory
4
6
  import android.graphics.Canvas
5
7
  import android.graphics.Paint
8
+ import android.graphics.drawable.BitmapDrawable
6
9
  import android.graphics.drawable.Drawable
7
- import android.net.Uri
10
+ import android.text.Editable
11
+ import android.text.Spannable
8
12
  import android.text.style.ImageSpan
13
+ import android.util.Log
14
+ import androidx.core.graphics.drawable.DrawableCompat
9
15
  import androidx.core.graphics.withSave
10
16
  import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
11
- import com.swmansion.enriched.styles.HtmlStyle
17
+ import com.swmansion.enriched.utils.AsyncDrawable
18
+ import androidx.core.graphics.drawable.toDrawable
19
+ import com.swmansion.enriched.R
20
+ import com.swmansion.enriched.spans.utils.ForceRedrawSpan
21
+ import com.swmansion.enriched.utils.ResourceManager
12
22
 
13
23
  class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
14
- private var htmlStyle: HtmlStyle? = null
24
+ private var width: Int = 0
25
+ private var height: Int = 0
15
26
 
16
- constructor(context: Context, uri: Uri, htmlStyle: HtmlStyle, ) : super(context, uri, ALIGN_BASELINE) {
17
- this.htmlStyle = htmlStyle
18
- }
19
-
20
- constructor(drawable: Drawable, source: String, htmlStyle: HtmlStyle) : super(drawable, source, ALIGN_BASELINE) {
21
- this.htmlStyle = htmlStyle
27
+ constructor(drawable: Drawable, source: String, width: Int, height: Int) : super(drawable, source, ALIGN_BASELINE) {
28
+ this.width = width
29
+ this.height = height
22
30
  }
23
31
 
24
32
  override fun draw(
@@ -35,7 +43,120 @@ class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
35
43
 
36
44
  override fun getDrawable(): Drawable {
37
45
  val drawable = super.getDrawable()
38
- drawable.setBounds(0, 0, htmlStyle!!.imgWidth, htmlStyle!!.imgHeight)
46
+ val scale = Resources.getSystem().displayMetrics.density
47
+
48
+ drawable.setBounds(0, 0, (width * scale).toInt() , (height * scale).toInt())
39
49
  return drawable
40
50
  }
51
+
52
+ override fun getSize(
53
+ paint: Paint,
54
+ text: CharSequence?,
55
+ start: Int,
56
+ end: Int,
57
+ fm: Paint.FontMetricsInt?
58
+ ): Int {
59
+ val d = drawable
60
+ val rect = d.bounds
61
+
62
+ if (fm != null) {
63
+ val imageHeight = rect.bottom - rect.top
64
+
65
+ // We want the image bottom to sit on the baseline (0).
66
+ // Therefore, the image top will be at: -imageHeight.
67
+ val targetTop = -imageHeight
68
+
69
+ // Expand the line UPWARDS if the image is taller than the current font
70
+ if (targetTop < fm.ascent) {
71
+ fm.ascent = targetTop
72
+ fm.top = targetTop
73
+ }
74
+ }
75
+
76
+ return rect.right
77
+ }
78
+
79
+ private fun registerDrawableLoadCallback (d: AsyncDrawable, text: Editable?) {
80
+ d.onLoaded = onLoaded@{
81
+ val spannable = text as? Spannable
82
+
83
+ if (spannable == null) {
84
+ return@onLoaded
85
+ }
86
+
87
+ val start = spannable.getSpanStart(this@EnrichedImageSpan)
88
+ val end = spannable.getSpanEnd(this@EnrichedImageSpan)
89
+
90
+ if (start != -1 && end != -1) {
91
+ // trick for adding empty span to force redraw when image is loaded
92
+ val redrawSpan = ForceRedrawSpan()
93
+ spannable.setSpan(redrawSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
94
+ spannable.removeSpan(redrawSpan)
95
+ }
96
+ }
97
+ }
98
+
99
+ fun observeAsyncDrawableLoaded(text: Editable?) {
100
+ val d = drawable
101
+
102
+ if (d !is AsyncDrawable) {
103
+ return
104
+ }
105
+
106
+ registerDrawableLoadCallback(d, text)
107
+
108
+ // If it's already loaded (race condition), run logic immediately
109
+ if (d.isLoaded) {
110
+ d.onLoaded?.invoke()
111
+ }
112
+ }
113
+
114
+ fun getWidth(): Int {
115
+ return width
116
+ }
117
+
118
+ fun getHeight(): Int {
119
+ return height
120
+ }
121
+
122
+ companion object {
123
+ fun createEnrichedImageSpan(src: String, width: Int, height: Int): EnrichedImageSpan {
124
+ var imgDrawable = prepareDrawableForImage(src)
125
+
126
+ if (imgDrawable == null) {
127
+ imgDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
128
+ }
129
+
130
+ return EnrichedImageSpan(imgDrawable, src, width, height)
131
+ }
132
+
133
+ private fun prepareDrawableForImage(src: String): Drawable? {
134
+ var cleanPath = src
135
+
136
+ if (cleanPath.startsWith("http://") || cleanPath.startsWith("https://")) {
137
+ return AsyncDrawable(cleanPath)
138
+ }
139
+
140
+ if (cleanPath.startsWith("file://")) {
141
+ cleanPath = cleanPath.substring(7)
142
+ }
143
+
144
+ var drawable: BitmapDrawable? = null
145
+
146
+ try {
147
+ val bitmap = BitmapFactory.decodeFile(cleanPath)
148
+ if (bitmap != null) {
149
+ drawable = bitmap.toDrawable(Resources.getSystem())
150
+ // set bounds so it knows how big it is naturally,
151
+ // though EnrichedImageSpan will override this with the HTML width/height later.
152
+ drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight())
153
+ }
154
+ } catch (e: Exception) {
155
+ // Failed to load file
156
+ Log.e("EnrichedImageSpan", "Failed to load image from path: $cleanPath", e)
157
+ }
158
+
159
+ return drawable
160
+ }
161
+ }
41
162
  }
@@ -123,4 +123,8 @@ object EnrichedSpans {
123
123
  else -> null
124
124
  }
125
125
  }
126
+
127
+ fun isTypeContinuous(type: Class<*>): Boolean {
128
+ return paragraphSpans.values.find { it.clazz == type }?.isContinuous == true
129
+ }
126
130
  }
@@ -0,0 +1,13 @@
1
+ package com.swmansion.enriched.spans.utils
2
+
3
+ import android.text.TextPaint
4
+ import android.text.style.MetricAffectingSpan
5
+
6
+ class ForceRedrawSpan: MetricAffectingSpan() {
7
+ override fun updateMeasureState(tp: TextPaint) {
8
+ // Do nothing, we don't actually want to change how it looks
9
+ }
10
+ override fun updateDrawState(tp: TextPaint?) {
11
+ // Do nothing
12
+ }
13
+ }
@@ -43,9 +43,6 @@ class HtmlStyle {
43
43
  var ulBulletSize: Int = 8
44
44
  var ulBulletColor: Int = Color.BLACK
45
45
 
46
- var imgWidth: Int = 200
47
- var imgHeight: Int = 200
48
-
49
46
  var aColor: Int = Color.BLACK
50
47
  var aUnderline: Boolean = true
51
48
 
@@ -100,10 +97,6 @@ class HtmlStyle {
100
97
  ulMarginLeft = parseFloat(ulStyle, "marginLeft").toInt()
101
98
  ulBulletSize = parseFloat(ulStyle, "bulletSize").toInt()
102
99
 
103
- val imgStyle = style.getMap("img")
104
- imgWidth = parseFloat(imgStyle, "width").toInt()
105
- imgHeight = parseFloat(imgStyle, "height").toInt()
106
-
107
100
  val aStyle = style.getMap("a")
108
101
  aColor = parseColor(aStyle, "color")
109
102
  aUnderline = parseIsUnderline(aStyle)
@@ -124,8 +117,8 @@ class HtmlStyle {
124
117
  private fun parseFloat(map: ReadableMap?, key: String): Float {
125
118
  val safeMap = ensureValueIsSet(map, key)
126
119
 
127
- val fontSize = safeMap.getDouble(key)
128
- return ceil(PixelUtil.toPixelFromSP(fontSize))
120
+ val value = safeMap.getDouble(key)
121
+ return ceil(PixelUtil.toPixelFromSP(value))
129
122
  }
130
123
 
131
124
  private fun parseColorWithOpacity(map: ReadableMap?, key: String, opacity: Int): Int {
@@ -54,6 +54,7 @@ class InlineStyles(private val view: EnrichedTextInputView) {
54
54
  val spanEnd = spannable.getSpanEnd(span)
55
55
  var finalStart: Int? = null
56
56
  var finalEnd: Int? = null
57
+ if (spanStart == -1 || spanEnd == -1) continue
57
58
 
58
59
  spannable.removeSpan(span)
59
60
 
@@ -3,13 +3,86 @@ package com.swmansion.enriched.styles
3
3
  import android.text.Editable
4
4
  import android.text.Spannable
5
5
  import android.text.SpannableStringBuilder
6
+ import android.util.Log
6
7
  import com.swmansion.enriched.EnrichedTextInputView
8
+ import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan
9
+ import com.swmansion.enriched.spans.EnrichedCodeBlockSpan
7
10
  import com.swmansion.enriched.spans.EnrichedSpans
8
11
  import com.swmansion.enriched.utils.getParagraphBounds
9
12
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
10
13
 
11
14
  class ParagraphStyles(private val view: EnrichedTextInputView) {
15
+ private fun <T>getPreviousParagraphSpan(spannable: Spannable, paragraphStart: Int, type: Class<T>): T? {
16
+ if (paragraphStart <= 0) return null
17
+
18
+ val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(paragraphStart - 1)
19
+ val spans = spannable.getSpans(previousParagraphStart, previousParagraphEnd, type)
20
+
21
+ // A paragraph implies a single cohesive style. having multiple spans of the
22
+ // same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
23
+ if (spans.size > 1) {
24
+ Log.w("ParagraphStyles", "getPreviousParagraphSpan(): Found more than one span in the paragraph!")
25
+ }
26
+
27
+ if (spans.isNotEmpty()) {
28
+ return spans.first()
29
+ }
30
+
31
+ return null
32
+ }
33
+
34
+ private fun <T>getNextParagraphSpan(spannable: Spannable, paragraphEnd: Int, type: Class<T>): T? {
35
+ if (paragraphEnd >= spannable.length - 1) return null
36
+
37
+ val (nextParagraphStart, nextParagraphEnd) = spannable.getParagraphBounds(paragraphEnd + 1)
38
+
39
+ val spans = spannable.getSpans(nextParagraphStart, nextParagraphEnd, type)
40
+
41
+ // A paragraph implies a single cohesive style. having multiple spans of the
42
+ // same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
43
+ if (spans.size > 1) {
44
+ Log.w("ParagraphStyles", "getNextParagraphSpan(): Found more than one span in the paragraph!")
45
+ }
46
+
47
+ if (spans.isNotEmpty()) {
48
+ return spans.first()
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * Applies a continuous span to the specified range.
56
+ * If the new range touches existing continuous spans, they are coalesced into a single span
57
+ */
58
+ private fun <T>setContinuousSpan(spannable: Spannable, start: Int, end: Int, type: Class<T>) {
59
+ val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
60
+ val previousSpan = getPreviousParagraphSpan(spannable, start, type)
61
+ val nextSpan = getNextParagraphSpan(spannable, end, type)
62
+ var newStart = start
63
+ var newEnd = end
64
+
65
+ if (previousSpan != null) {
66
+ newStart = spannable.getSpanStart(previousSpan)
67
+ spannable.removeSpan(previousSpan)
68
+ }
69
+
70
+ if (nextSpan != null && start != end) {
71
+ newEnd = spannable.getSpanEnd(nextSpan)
72
+ spannable.removeSpan(nextSpan)
73
+ }
74
+
75
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd)
76
+ spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
77
+ }
78
+
79
+
12
80
  private fun <T>setSpan(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
81
+ if (EnrichedSpans.isTypeContinuous(type)) {
82
+ setContinuousSpan(spannable, start, end, type)
83
+ return
84
+ }
85
+
13
86
  val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
14
87
  val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
15
88
  spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -94,6 +167,33 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
94
167
  return spans.isNotEmpty()
95
168
  }
96
169
 
170
+ private fun <T>mergeAdjacentStyleSpans(s: Editable, endCursorPosition: Int, type: Class<T>) {
171
+ val (start, end) = s.getParagraphBounds(endCursorPosition)
172
+ val currParagraphSpans = s.getSpans(start, end, type)
173
+
174
+ if (currParagraphSpans.isEmpty()) {
175
+ return
176
+ }
177
+
178
+ val currSpan = currParagraphSpans[0]
179
+ val nextSpan = getNextParagraphSpan(s, end, type)
180
+
181
+ if (nextSpan == null) {
182
+ return
183
+ }
184
+
185
+ val newStart = s.getSpanStart(currSpan)
186
+ val newEnd = s.getSpanEnd(nextSpan)
187
+
188
+ s.removeSpan(nextSpan)
189
+ s.removeSpan(currSpan)
190
+
191
+ val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd)
192
+ val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
193
+
194
+ s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
195
+ }
196
+
97
197
  fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
98
198
  var endCursorPosition = endPosition
99
199
  val isBackspace = s.length < previousTextLength
@@ -101,7 +201,14 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
101
201
 
102
202
  for ((style, config) in EnrichedSpans.paragraphSpans) {
103
203
  val spanState = view.spanState ?: continue
104
- val styleStart = spanState.getStart(style) ?: continue
204
+ val styleStart = spanState.getStart(style)
205
+
206
+ if (styleStart == null) {
207
+ if (config.isContinuous) {
208
+ mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
209
+ }
210
+ continue
211
+ }
105
212
 
106
213
  if (isNewLine) {
107
214
  if (!config.isContinuous) {
@@ -154,8 +261,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
154
261
 
155
262
  if (start == end) {
156
263
  spannable.insert(start, "\u200B")
157
- view.spanState?.setStart(name, start + 1)
158
264
  setAndMergeSpans(spannable, type, start, end + 1)
265
+ view.selection.validateStyles()
159
266
 
160
267
  return
161
268
  }
@@ -170,8 +277,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
170
277
  currentStart = currentEnd + 1
171
278
  }
172
279
 
173
- view.spanState?.setStart(name, start)
174
280
  setAndMergeSpans(spannable, type, start, currentEnd)
281
+ view.selection.validateStyles()
175
282
  }
176
283
 
177
284
  fun getStyleRange(): Pair<Int, Int> {