From aee9b96b81f91e8d8380d816a3eb8e80c1558ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 17 Nov 2025 15:06:51 +0100 Subject: [PATCH 1/5] fix: handle style conflicts during automatic links detection --- .../swmansion/enriched/spans/EnrichedSpans.kt | 12 +++++++--- .../enriched/styles/ParametrizedStyles.kt | 23 ++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt index 85e47b28c..493477978 100644 --- a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +++ b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt @@ -1,8 +1,13 @@ package com.swmansion.enriched.spans -data class BaseSpanConfig(val clazz: Class<*>) -data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean) -data class ListSpanConfig(val clazz: Class<*>, val shortcut: String) +interface ISpanConfig { + val clazz: Class<*> +} + +data class BaseSpanConfig(override val clazz: Class<*>): ISpanConfig +data class ParagraphSpanConfig(override val clazz: Class<*>, val isContinuous: Boolean): ISpanConfig +data class ListSpanConfig(override val clazz: Class<*>, val shortcut: String) : ISpanConfig + data class StylesMergingConfig( // styles that should be removed when we apply specific style @@ -62,6 +67,7 @@ object EnrichedSpans { MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java), ) + val allSpans: Map = inlineSpans + paragraphSpans + listSpans + parametrizedStyles val mergingConfig: Map = mapOf( BOLD to StylesMergingConfig( blockingStyles = arrayOf(CODE_BLOCK) diff --git a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt index cdadbcad6..c0771fb32 100644 --- a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt @@ -104,15 +104,36 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { return Triple(result, start, end) } + private fun detectLinkConflicts(spannable: Spannable, start: Int, end: Int): Boolean { + val mergingConfig = EnrichedSpans.mergingConfig[EnrichedSpans.LINK] ?: return false + val conflictingStyles = mergingConfig.conflictingStyles + val blockingStyles = mergingConfig.blockingStyles + + for (style in blockingStyles) { + val spans = spannable.getSpans(start, end, EnrichedSpans.allSpans[style]?.clazz) + if (spans.isNotEmpty()) return true + } + + for (style in conflictingStyles) { + val spans = spannable.getSpans(start, end, EnrichedSpans.allSpans[style]?.clazz) + if (spans.isNotEmpty()) return true + } + + return false + } + private fun afterTextChangedLinks(result: Triple) { // Do not detect link if it's applied manually if (isSettingLinkSpan) return val spannable = view.text as Spannable val (word, start, end) = result + if (detectLinkConflicts(spannable, start, end)) { + return + } + // TODO: Consider using more reliable regex, this one matches almost anything val urlPattern = android.util.Patterns.WEB_URL.matcher(word) - val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java) for (span in spans) { spannable.removeSpan(span) From 8abc4b0c693b4c0629efcfa1fdd314fd51d5a6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 17 Nov 2025 15:29:15 +0100 Subject: [PATCH 2/5] fix: use spanState variable --- .../swmansion/enriched/spans/EnrichedSpans.kt | 11 +++-------- .../enriched/styles/ParametrizedStyles.kt | 16 +++++++--------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt index 493477978..df469c7f9 100644 --- a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +++ b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt @@ -1,12 +1,8 @@ package com.swmansion.enriched.spans -interface ISpanConfig { - val clazz: Class<*> -} - -data class BaseSpanConfig(override val clazz: Class<*>): ISpanConfig -data class ParagraphSpanConfig(override val clazz: Class<*>, val isContinuous: Boolean): ISpanConfig -data class ListSpanConfig(override val clazz: Class<*>, val shortcut: String) : ISpanConfig +data class BaseSpanConfig(val clazz: Class<*>) +data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean) +data class ListSpanConfig(val clazz: Class<*>, val shortcut: String) data class StylesMergingConfig( @@ -67,7 +63,6 @@ object EnrichedSpans { MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java), ) - val allSpans: Map = inlineSpans + paragraphSpans + listSpans + parametrizedStyles val mergingConfig: Map = mapOf( BOLD to StylesMergingConfig( blockingStyles = arrayOf(CODE_BLOCK) diff --git a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt index c0771fb32..d87410ac4 100644 --- a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt @@ -104,34 +104,32 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { return Triple(result, start, end) } - private fun detectLinkConflicts(spannable: Spannable, start: Int, end: Int): Boolean { + private fun detectLinkConflicts(): Boolean { val mergingConfig = EnrichedSpans.mergingConfig[EnrichedSpans.LINK] ?: return false val conflictingStyles = mergingConfig.conflictingStyles val blockingStyles = mergingConfig.blockingStyles for (style in blockingStyles) { - val spans = spannable.getSpans(start, end, EnrichedSpans.allSpans[style]?.clazz) - if (spans.isNotEmpty()) return true + if (view.spanState?.getStart(style) != null) return true } for (style in conflictingStyles) { - val spans = spannable.getSpans(start, end, EnrichedSpans.allSpans[style]?.clazz) - if (spans.isNotEmpty()) return true + if (view.spanState?.getStart(style) != null) return true } return false } private fun afterTextChangedLinks(result: Triple) { + if (detectLinkConflicts()) { + return + } + // Do not detect link if it's applied manually if (isSettingLinkSpan) return val spannable = view.text as Spannable val (word, start, end) = result - if (detectLinkConflicts(spannable, start, end)) { - return - } - // TODO: Consider using more reliable regex, this one matches almost anything val urlPattern = android.util.Patterns.WEB_URL.matcher(word) val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java) From cbd86ccf814fa3b723ed50fb6496ec7ed20985a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 17 Nov 2025 15:30:18 +0100 Subject: [PATCH 3/5] fix: remove newline --- .../src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt index df469c7f9..85e47b28c 100644 --- a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +++ b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt @@ -4,7 +4,6 @@ data class BaseSpanConfig(val clazz: Class<*>) data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean) data class ListSpanConfig(val clazz: Class<*>, val shortcut: String) - data class StylesMergingConfig( // styles that should be removed when we apply specific style val conflictingStyles: Array = emptyArray(), From bb4d93e71a2537accce7b42bfa031fb87a22de4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Mon, 17 Nov 2025 16:13:00 +0100 Subject: [PATCH 4/5] fix: getting merging config --- .../java/com/swmansion/enriched/styles/ParametrizedStyles.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt index d87410ac4..d15a457f1 100644 --- a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt @@ -105,7 +105,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { } private fun detectLinkConflicts(): Boolean { - val mergingConfig = EnrichedSpans.mergingConfig[EnrichedSpans.LINK] ?: return false + val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return false val conflictingStyles = mergingConfig.conflictingStyles val blockingStyles = mergingConfig.blockingStyles From e7a503989ba9d53288fc953c7722979740b2a496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=C5=BB=C3=B3=C5=82kiewski?= Date: Tue, 18 Nov 2025 08:18:05 +0100 Subject: [PATCH 5/5] fix: rename detectLinkConflicts function --- .../enriched/styles/ParametrizedStyles.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt index d15a457f1..9b8665606 100644 --- a/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt @@ -104,29 +104,26 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) { return Triple(result, start, end) } - private fun detectLinkConflicts(): Boolean { - val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return false + private fun canLinkBeApplied(): Boolean { + val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true val conflictingStyles = mergingConfig.conflictingStyles val blockingStyles = mergingConfig.blockingStyles for (style in blockingStyles) { - if (view.spanState?.getStart(style) != null) return true + if (view.spanState?.getStart(style) != null) return false } for (style in conflictingStyles) { - if (view.spanState?.getStart(style) != null) return true + if (view.spanState?.getStart(style) != null) return false } - return false + return true } private fun afterTextChangedLinks(result: Triple) { - if (detectLinkConflicts()) { - return - } - // Do not detect link if it's applied manually - if (isSettingLinkSpan) return + if (isSettingLinkSpan || !canLinkBeApplied()) return + val spannable = view.text as Spannable val (word, start, end) = result