diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt index 27bdc52cb37..499b89fa54d 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt @@ -1,9 +1,13 @@ package org.oppia.android.util.parser.html +import android.graphics.Typeface import android.text.Editable +import android.text.Layout import android.text.Spannable import android.text.SpannableStringBuilder +import android.text.style.AlignmentSpan import android.text.style.ImageSpan +import android.text.style.StyleSpan import org.oppia.android.util.logging.ConsoleLogger import org.xml.sax.Attributes @@ -11,6 +15,7 @@ import org.xml.sax.Attributes const val CUSTOM_IMG_TAG = "oppia-noninteractive-image" private const val CUSTOM_IMG_FILE_PATH_ATTRIBUTE = "filepath-with-value" private const val CUSTOM_IMG_ALT_TEXT_ATTRIBUTE = "alt-with-value" +private const val CUSTOM_IMG_CAPTION_ATTRIBUTE = "caption-with-value" /** * A custom tag handler for supporting custom Oppia images parsed with [CustomHtmlContentHandler]. @@ -27,6 +32,8 @@ class ImageTagHandler( ) { val source = attributes.getJsonStringValue(CUSTOM_IMG_FILE_PATH_ATTRIBUTE) val contentDescription = attributes.getJsonStringValue(CUSTOM_IMG_ALT_TEXT_ATTRIBUTE) + val caption = attributes.getJsonStringValue(CUSTOM_IMG_CAPTION_ATTRIBUTE) + if (source != null) { val (startIndex, endIndex) = output.run { // Use a control character to ensure that there's at least 1 character on which to "attach" @@ -57,6 +64,41 @@ class ImageTagHandler( Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) output.replace(openIndex, output.length, spannableBuilder) - } else consoleLogger.w("ImageTagHandler", "Failed to parse $CUSTOM_IMG_ALT_TEXT_ATTRIBUTE") + } else consoleLogger.w( + "ImageTagHandler", + "Failed to parse $CUSTOM_IMG_ALT_TEXT_ATTRIBUTE" + ) + if (!caption.isNullOrBlank()) { + output.append("\n") + val captionStart = output.length + output.append(caption) + val captionEnd = output.length + output.setSpan( + StyleSpan(Typeface.ITALIC), + captionStart, + captionEnd, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE + ) + output.setSpan( + AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), + captionStart, + captionEnd, + Spannable.SPAN_PARAGRAPH + ) + + // Insert a newline after the caption and reset alignment. + output.append("\n") + val resetStart = output.length + output.append(" ") + output.setSpan( + AlignmentSpan.Standard(Layout.Alignment.ALIGN_NORMAL), + resetStart, + output.length, + Spannable.SPAN_PARAGRAPH + ) + } else consoleLogger.w( + "ImageTagHandler", + "Failed to parse $CUSTOM_IMG_CAPTION_ATTRIBUTE" + ) } } diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt index 17219b230f7..e1bd9e22a2e 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt @@ -2,9 +2,13 @@ package org.oppia.android.util.parser.html import android.app.Application import android.content.Context +import android.graphics.Typeface import android.text.Html +import android.text.Layout import android.text.Spannable +import android.text.style.AlignmentSpan import android.text.style.ImageSpan +import android.text.style.StyleSpan import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -70,6 +74,11 @@ private const val IMAGE_TAG_WITH_SPACE_ONLY_ALT_VALUE_MARKUP = "caption-with-value=\"""\" " + "filepath-with-value=\""test_image1.png"\">" +private const val IMAGE_TAG_WITH_CAPTION_MARKUP = + "" + /** Tests for [ImageTagHandler]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @@ -99,6 +108,53 @@ class ImageTagHandlerTest { // TODO(#3085): Introduce test for verifying that the error log scenario is logged correctly. + @Test + fun testParseHtml_withImageCardMarkup_withCaption_addsCaptionWithStyleAndAlignment() { + val parsedHtml = CustomHtmlContentHandler.fromHtml( + html = IMAGE_TAG_WITH_CAPTION_MARKUP, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithImageTagSupport + ) + + val parsedHtmlString = parsedHtml.toString() + assertThat(parsedHtmlString).contains("Sample Caption") + + val styleSpans = parsedHtml.getSpans( + /* start = */ 0, + /* end = */ parsedHtml.length, + StyleSpan::class.java + ) + assertThat(styleSpans).hasLength(1) + assertThat(styleSpans[0].style).isEqualTo(Typeface.ITALIC) + + val alignmentSpans = parsedHtml.getSpans( + /* start = */ 0, + /* end = */ parsedHtml.length, + AlignmentSpan.Standard::class.java + ) + assertThat(alignmentSpans).hasLength(2) + + // Check the first AlignmentSpan for center alignment (caption) + assertThat(alignmentSpans[0].alignment).isEqualTo(Layout.Alignment.ALIGN_CENTER) + + // Check the second AlignmentSpan for normal alignment (reset) + assertThat(alignmentSpans[1].alignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL) + } + + @Test + fun testParseHtml_withMultipleImages_withCaptions_includesAllCaptions() { + val parsedHtml = + CustomHtmlContentHandler.fromHtml( + html = "$IMAGE_TAG_WITH_CAPTION_MARKUP and $IMAGE_TAG_WITH_CAPTION_MARKUP", + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithImageTagSupport + ) + + val parsedHtmlStr = parsedHtml.toString() + assertThat(parsedHtmlStr).contains("Sample Caption") + assertThat(parsedHtmlStr).contains("Sample Caption") + } + @Test fun testParseHtml_emptyString_doesNotIncludeImageSpan() { val parsedHtml =