From 1273c3147b96327dd011cd69fd2b964983f4f3f9 Mon Sep 17 00:00:00 2001 From: Manas <119405883+manas-yu@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:09:54 +0530 Subject: [PATCH] Fix #3188: Added caption-with-value in ImageTagHandler (#5593) ## Explanation Fix #3188 This PR enhances the `ImageTagHandler` to support captions in the `oppia-noninteractive-image` custom tag. - The caption-with-value attribute is now parsed. - The caption text is included as readable text following the image. ## Before ![IMG-20241214-WA0001](https://github.com/user-attachments/assets/746dd49c-f54f-47a8-aebb-dec821e057be) ![IMG-20241214-WA0003](https://github.com/user-attachments/assets/20a73d23-6987-4cab-811b-32313eff3076) ## After ![IMG-20241214-WA0005](https://github.com/user-attachments/assets/044d93ec-75c8-44a7-ad11-6bea16a39651) ![WhatsApp Image 2024-12-17 at 19 25 11_ae9c0cb6](https://github.com/user-attachments/assets/f45c01c3-23d9-4781-b26d-c6076e96828e) ![IMG-20241214-WA0006](https://github.com/user-attachments/assets/6f0d3204-5119-4d45-bfb6-3831529b1ee7) ![WhatsApp Image 2024-12-17 at 19 25 11_1c80032d](https://github.com/user-attachments/assets/a53b8143-6a24-4363-9c84-3e5702911fb2) ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --- .../util/parser/html/ImageTagHandler.kt | 44 ++++++++++++++- .../util/parser/html/ImageTagHandlerTest.kt | 56 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) 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=\"&quot;&quot;\" " + "filepath-with-value=\"&quot;test_image1.png&quot;\">" +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 =