react-native-enriched 0.1.5 → 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 (80) hide show
  1. package/README.md +3 -9
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
  6. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
  8. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
  9. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  10. package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
  11. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
  14. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  15. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +2 -9
  16. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  17. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +110 -3
  18. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
  19. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  20. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
  21. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  22. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -1
  23. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
  24. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
  25. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
  26. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
  27. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
  28. package/android/src/main/res/drawable/broken_image.xml +10 -0
  29. package/ios/EnrichedTextInputView.h +3 -1
  30. package/ios/EnrichedTextInputView.mm +167 -68
  31. package/ios/config/InputConfig.h +6 -0
  32. package/ios/config/InputConfig.mm +32 -0
  33. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
  34. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
  35. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
  36. package/ios/inputParser/InputParser.mm +179 -31
  37. package/ios/inputTextView/InputTextView.mm +3 -5
  38. package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
  39. package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
  40. package/ios/styles/BlockQuoteStyle.mm +5 -26
  41. package/ios/styles/BoldStyle.mm +2 -0
  42. package/ios/styles/CodeBlockStyle.mm +228 -0
  43. package/ios/styles/H1Style.mm +1 -0
  44. package/ios/styles/H2Style.mm +1 -0
  45. package/ios/styles/H3Style.mm +1 -0
  46. package/ios/styles/ImageStyle.mm +158 -0
  47. package/ios/styles/InlineCodeStyle.mm +2 -0
  48. package/ios/styles/ItalicStyle.mm +2 -0
  49. package/ios/styles/LinkStyle.mm +15 -7
  50. package/ios/styles/MentionStyle.mm +133 -36
  51. package/ios/styles/OrderedListStyle.mm +5 -8
  52. package/ios/styles/StrikethroughStyle.mm +2 -0
  53. package/ios/styles/UnderlineStyle.mm +2 -0
  54. package/ios/styles/UnorderedListStyle.mm +5 -8
  55. package/ios/utils/BaseStyleProtocol.h +1 -0
  56. package/ios/utils/ImageData.h +10 -0
  57. package/ios/utils/ImageData.mm +4 -0
  58. package/ios/utils/LayoutManagerExtension.mm +118 -3
  59. package/ios/utils/OccurenceUtils.h +4 -0
  60. package/ios/utils/OccurenceUtils.mm +47 -0
  61. package/ios/utils/ParagraphAttributesUtils.h +1 -0
  62. package/ios/utils/ParagraphAttributesUtils.mm +87 -20
  63. package/ios/utils/StringExtension.h +1 -1
  64. package/ios/utils/StringExtension.mm +17 -8
  65. package/ios/utils/StyleHeaders.h +12 -0
  66. package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
  67. package/lib/module/EnrichedTextInput.js +4 -2
  68. package/lib/module/EnrichedTextInput.js.map +1 -1
  69. package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
  70. package/lib/module/normalizeHtmlStyle.js +0 -4
  71. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  72. package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
  73. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  74. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
  75. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  76. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/EnrichedTextInput.tsx +6 -7
  79. package/src/EnrichedTextInputNativeComponent.ts +7 -5
  80. package/src/normalizeHtmlStyle.ts +0 -4
@@ -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
  }
@@ -1,5 +1,7 @@
1
1
  package com.swmansion.enriched.spans
2
2
 
3
+ import com.swmansion.enriched.styles.HtmlStyle
4
+
3
5
  data class BaseSpanConfig(val clazz: Class<*>)
4
6
  data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
5
7
  data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
@@ -62,50 +64,67 @@ object EnrichedSpans {
62
64
  MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
63
65
  )
64
66
 
65
- val mergingConfig: Map<String, StylesMergingConfig> = mapOf(
66
- BOLD to StylesMergingConfig(
67
- blockingStyles = arrayOf(CODE_BLOCK)
68
- ),
69
- ITALIC to StylesMergingConfig(
70
- blockingStyles = arrayOf(CODE_BLOCK)
71
- ),
72
- UNDERLINE to StylesMergingConfig(
73
- blockingStyles = arrayOf(CODE_BLOCK)
74
- ),
75
- STRIKETHROUGH to StylesMergingConfig(
76
- blockingStyles = arrayOf(CODE_BLOCK)
77
- ),
78
- INLINE_CODE to StylesMergingConfig(
79
- conflictingStyles = arrayOf(MENTION, LINK),
80
- blockingStyles = arrayOf(CODE_BLOCK)
81
- ),
82
- H1 to StylesMergingConfig(
83
- conflictingStyles = arrayOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
84
- ),
85
- H2 to StylesMergingConfig(
86
- conflictingStyles = arrayOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
87
- ),
88
- H3 to StylesMergingConfig(
89
- conflictingStyles = arrayOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK),
90
- ),
91
- BLOCK_QUOTE to StylesMergingConfig(
92
- conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST),
93
- ),
94
- CODE_BLOCK to StylesMergingConfig(
95
- conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE),
96
- ),
97
- UNORDERED_LIST to StylesMergingConfig(
98
- conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
99
- ),
100
- ORDERED_LIST to StylesMergingConfig(
101
- conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
102
- ),
103
- LINK to StylesMergingConfig(
104
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
105
- ),
106
- IMAGE to StylesMergingConfig(),
107
- MENTION to StylesMergingConfig(
108
- blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
109
- ),
110
- )
67
+ fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
68
+ return when (style) {
69
+ BOLD -> {
70
+ val blockingStyles = mutableListOf(CODE_BLOCK)
71
+ if (htmlStyle.h1Bold) blockingStyles.add(H1)
72
+ if (htmlStyle.h2Bold) blockingStyles.add(H2)
73
+ if (htmlStyle.h3Bold) blockingStyles.add(H3)
74
+ StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
75
+ }
76
+ ITALIC -> StylesMergingConfig(
77
+ blockingStyles = arrayOf(CODE_BLOCK)
78
+ )
79
+ UNDERLINE -> StylesMergingConfig(
80
+ blockingStyles = arrayOf(CODE_BLOCK)
81
+ )
82
+ STRIKETHROUGH -> StylesMergingConfig(
83
+ blockingStyles = arrayOf(CODE_BLOCK)
84
+ )
85
+ INLINE_CODE -> StylesMergingConfig(
86
+ conflictingStyles = arrayOf(MENTION, LINK),
87
+ blockingStyles = arrayOf(CODE_BLOCK)
88
+ )
89
+ H1 -> {
90
+ val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
91
+ if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
92
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
93
+ }
94
+ H2 -> {
95
+ val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
96
+ if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
97
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
98
+ }
99
+ H3 -> {
100
+ val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
101
+ if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
102
+ StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
103
+ }
104
+ BLOCK_QUOTE -> StylesMergingConfig(
105
+ conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
106
+ )
107
+ CODE_BLOCK -> StylesMergingConfig(
108
+ conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
109
+ )
110
+ UNORDERED_LIST -> StylesMergingConfig(
111
+ conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
112
+ )
113
+ ORDERED_LIST -> StylesMergingConfig(
114
+ conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
115
+ )
116
+ LINK -> StylesMergingConfig(
117
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
118
+ )
119
+ IMAGE -> StylesMergingConfig()
120
+ MENTION -> StylesMergingConfig(
121
+ blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
122
+ )
123
+ else -> null
124
+ }
125
+ }
126
+
127
+ fun isTypeContinuous(type: Class<*>): Boolean {
128
+ return paragraphSpans.values.find { it.clazz == type }?.isContinuous == true
129
+ }
111
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> {