Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,8 @@ object EnrichedSpans {
else -> null
}
}

fun isTypeContinuous(type: Class<*>): Boolean {
return paragraphSpans.values.find { it.clazz == type }?.isContinuous == true
}
}
113 changes: 110 additions & 3 deletions android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,86 @@ package com.swmansion.enriched.styles
import android.text.Editable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.util.Log
import com.swmansion.enriched.EnrichedTextInputView
import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan
import com.swmansion.enriched.spans.EnrichedCodeBlockSpan
import com.swmansion.enriched.spans.EnrichedSpans
import com.swmansion.enriched.utils.getParagraphBounds
import com.swmansion.enriched.utils.getSafeSpanBoundaries

class ParagraphStyles(private val view: EnrichedTextInputView) {
private fun <T>getPreviousParagraphSpan(spannable: Spannable, paragraphStart: Int, type: Class<T>): T? {
if (paragraphStart <= 0) return null

val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(paragraphStart - 1)
val spans = spannable.getSpans(previousParagraphStart, previousParagraphEnd, type)

// A paragraph implies a single cohesive style. having multiple spans of the
// same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
if (spans.size > 1) {
Log.w("ParagraphStyles", "getPreviousParagraphSpan(): Found more than one span in the paragraph!")
}

if (spans.isNotEmpty()) {
return spans.first()
}

return null
}

private fun <T>getNextParagraphSpan(spannable: Spannable, paragraphEnd: Int, type: Class<T>): T? {
if (paragraphEnd >= spannable.length - 1) return null

val (nextParagraphStart, nextParagraphEnd) = spannable.getParagraphBounds(paragraphEnd + 1)

val spans = spannable.getSpans(nextParagraphStart, nextParagraphEnd, type)

// A paragraph implies a single cohesive style. having multiple spans of the
// same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
if (spans.size > 1) {
Log.w("ParagraphStyles", "getNextParagraphSpan(): Found more than one span in the paragraph!")
}

if (spans.isNotEmpty()) {
return spans.first()
}

return null
}

/**
* Applies a continuous span to the specified range.
* If the new range touches existing continuous spans, they are coalesced into a single span
*/
private fun <T>setContinuousSpan(spannable: Spannable, start: Int, end: Int, type: Class<T>) {
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
val previousSpan = getPreviousParagraphSpan(spannable, start, type)
val nextSpan = getNextParagraphSpan(spannable, end, type)
var newStart = start
var newEnd = end

if (previousSpan != null) {
newStart = spannable.getSpanStart(previousSpan)
spannable.removeSpan(previousSpan)
}

if (nextSpan != null) {
newEnd = spannable.getSpanEnd(nextSpan)
spannable.removeSpan(nextSpan)
}

val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd)
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}


private fun <T>setSpan(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
if (EnrichedSpans.isTypeContinuous(type)) {
setContinuousSpan(spannable, start, end, type)
return
}

val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Expand Down Expand Up @@ -94,14 +167,48 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
return spans.isNotEmpty()
}

private fun <T>mergeAdjacentStyleSpans(s: Editable, endCursorPosition: Int, type: Class<T>) {
val (start, end) = s.getParagraphBounds(endCursorPosition)
val currParagraphSpans = s.getSpans(start, end, type)

if (currParagraphSpans.isEmpty()) {
return
}

val currSpan = currParagraphSpans[0]
val nextSpan = getNextParagraphSpan(s, end, type)

if (nextSpan == null) {
return
}

val newStart = s.getSpanStart(currSpan)
val newEnd = s.getSpanEnd(nextSpan)

s.removeSpan(nextSpan)
s.removeSpan(currSpan)

val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd)
val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)

s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}

fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
var endCursorPosition = endPosition
val isBackspace = s.length < previousTextLength
val isNewLine = endCursorPosition == 0 || endCursorPosition > 0 && s[endCursorPosition - 1] == '\n'

for ((style, config) in EnrichedSpans.paragraphSpans) {
val spanState = view.spanState ?: continue
val styleStart = spanState.getStart(style) ?: continue
val styleStart = spanState.getStart(style)

if (styleStart == null) {
if (config.isContinuous) {
mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
}
continue
}

if (isNewLine) {
if (!config.isContinuous) {
Expand Down Expand Up @@ -154,8 +261,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {

if (start == end) {
spannable.insert(start, "\u200B")
view.spanState?.setStart(name, start + 1)
setAndMergeSpans(spannable, type, start, end + 1)
view.selection.validateStyles()

return
}
Expand All @@ -170,8 +277,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
currentStart = currentEnd + 1
}

view.spanState?.setStart(name, start)
setAndMergeSpans(spannable, type, start, currentEnd)
view.selection.validateStyles()
}

fun getStyleRange(): Pair<Int, Int> {
Expand Down
Loading