diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt index bca56be075e..25cc7bf29d3 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt @@ -202,7 +202,7 @@ class CustomHtmlContentHandler private constructor( * Called when the closing of a custom tag is encountered. This does not support processing * attributes of the tag--[handleTag] should be used, instead. * - * This function will always be called before [handleClosingTag]. + * This function will always be called before [handleTag]. * * @param output the destination [Editable] to which spans can be added * @param indentation The zero-based indentation level of this item. diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt index 849f2e226be..28321115984 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt @@ -1,9 +1,13 @@ package org.oppia.android.util.parser.html import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint import android.text.Editable import android.text.Spannable import android.text.Spanned +import android.text.style.ImageSpan +import androidx.core.view.ViewCompat import org.oppia.android.util.locale.OppiaLocale import org.xml.sax.Attributes import java.util.Stack @@ -57,6 +61,76 @@ class LiTagHandler( } CUSTOM_LIST_LI_TAG -> latestPendingList?.closeItem(output) } + formatImageSpans(output) + } + + /** Formats ImageSpans to ensure they render as block images with line breaks. */ + private fun formatImageSpans(output: Editable) { + val imageSpans = output.getSpans(0, output.length, ImageSpan::class.java) + + imageSpans.sortedByDescending { output.getSpanStart(it) }.forEach { span -> + val startIndex = output.getSpanStart(span) + val endIndex = output.getSpanEnd(span) + + if (startIndex >= 0 && endIndex <= output.length) { + if (endIndex < output.length && output[endIndex] != '\n') { + output.insert(endIndex, "\n") + } + if (startIndex > 0 && output[startIndex - 1] != '\n') { + output.insert(startIndex, "\n") + } + + val currentStart = output.getSpanStart(span) + val currentEnd = output.getSpanEnd(span) + val leadingMargins = output.getSpans( + currentStart, + currentStart, + ListItemLeadingMarginSpan::class.java + ) + val totalMargin = leadingMargins.sumOf { it.getLeadingMargin(true) } + val isRtl by lazy { + displayLocale.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL + } + + output.removeSpan(span) + val customSpan = CustomImageSpan(span, totalMargin, isRtl) + output.setSpan( + customSpan, + currentStart, + currentEnd, + output.getSpanFlags(span) + ) + } + } + } + + /** + * Custom [ImageSpan] that shifts the drawing position by the total margin of parent list items. + */ + private class CustomImageSpan( + originalSpan: ImageSpan, + private val totalMargin: Int, + private val isRtl: Boolean + ) : ImageSpan(originalSpan.drawable, originalSpan.verticalAlignment) { + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + baseline: Int, + bottom: Int, + paint: Paint + ) { + val adjustedX = if (isRtl) { + x + totalMargin + } else { + x - totalMargin + } + super.draw(canvas, text, start, end, adjustedX, top, baseline, bottom, paint) + } } private sealed class ListTag, S : ListItemLeadingMarginSpan>( diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt index 487cda78b52..13246b7d2d8 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import android.text.Html import android.text.Spannable +import android.text.style.ImageSpan import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -113,6 +114,33 @@ class LiTagHandlerTest { .hasLength(2) } + @Test + fun testFromHtml_withImageSpanInsideList_insertsNewlinesAroundImage() { + val html = "TestImage" + val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + val liTaghandler = LiTagHandler(context, displayLocale) + val parsedHtml = + CustomHtmlContentHandler.fromHtml( + html = html, + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf( + CUSTOM_LIST_LI_TAG to liTaghandler, + CUSTOM_LIST_UL_TAG to liTaghandler + ) + ) + + val imageSpans = parsedHtml.getSpans(0, parsedHtml.length, ImageSpan::class.java) + assertThat(imageSpans).hasLength(1) + val imageSpan = imageSpans[0] + val start = parsedHtml.getSpanStart(imageSpan) + val end = parsedHtml.getSpanEnd(imageSpan) + + assertThat(parsedHtml[start - 1]).isEqualTo('\n') + assertThat(parsedHtml[end]).isEqualTo('\n') + assertThat(parsedHtml.toString()).contains("Test") + assertThat(parsedHtml.toString()).contains("Image") + } + @Test fun testCustomListElement_betweenNestedParagraphs_parsesCorrectlyIntoNumberedListSpan() { val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT)