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
12 changes: 12 additions & 0 deletions apps/mobile/modules/t3-markdown-text/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ android {
namespace 'expo.modules.t3markdowntext'
compileSdk rootProject.ext.compileSdkVersion

testOptions {
unitTests.includeAndroidResources = true
}

defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
Expand All @@ -17,4 +21,12 @@ android {
dependencies {
implementation project(':expo-modules-core')
implementation 'com.facebook.react:react-android'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
}

tasks.withType(Test).configureEach {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ private object MarkdownSpannableFactory : Spannable.Factory() {
SpannableStringBuilder(source)
}

private fun copyTextWithoutInlineImages(
internal fun copyTextWithoutInlineImages(
text: CharSequence,
start: Int,
end: Int
): String {
if (text !is Spanned) return text.subSequence(start, end).toString()

fun isInlineImage(index: Int): Boolean =
index >= 0 && text[index].toString() == OBJECT_REPLACEMENT_CHARACTER &&
text.getSpans(index, index + 1, ReplacementSpan::class.java).isNotEmpty()

return buildString {
for (index in start until end) {
val isInlineImage =
text[index].toString() == OBJECT_REPLACEMENT_CHARACTER &&
text.getSpans(index, index + 1, ReplacementSpan::class.java).isNotEmpty()
if (!isInlineImage) append(text[index])
// The renderer inserts one NBSP after each image to keep its label on the same line.
// Inspect the original text even when selection starts after the image.
val isIconSpacer = text[index] == '\u00A0' && isInlineImage(index - 1)
if (!isInlineImage(index) && !isIconSpacer) append(text[index])
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package expo.modules.t3markdowntext

import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ImageSpan
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [36], manifest = Config.NONE)
class MarkdownSelectionCopyTest {
private fun withIcon(value: String): SpannableString = SpannableString(value).apply {
val index = value.indexOf('\uFFFC')
setSpan(ImageSpan(ColorDrawable()), index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}

@Test
fun removesIconAndInjectedSpacer() {
val text = withIcon("\uFFFC\u00A0main.go:12 starts the server.")
assertEquals("main.go:12 starts the server.", copyTextWithoutInlineImages(text, 0, text.length))
}

@Test
fun removesSpacerWhenSelectionStartsAfterIcon() {
val text = withIcon("\uFFFC\u00A0main.go:12 starts the server.")
assertEquals("main.go:12", copyTextWithoutInlineImages(text, 1, 12))
}

@Test
fun preservesAuthoredWhitespaceAndLiteralObjectCharacters() {
val text = withIcon("before\u00A0 \uFFFC\u00A0\u00A0 main.go after\u00A0\uFFFC\u00A0")
assertEquals(
"before\u00A0 \u00A0 main.go after\u00A0\uFFFC\u00A0",
copyTextWithoutInlineImages(text, 0, text.length)
)
}

@Test
fun preservesTextWithoutImageSpans() {
val text = "\uFFFC\u00A0main.go"
assertEquals(text, copyTextWithoutInlineImages(text, 0, text.length))
assertEquals(text, copyTextWithoutInlineImages(SpannableString(text), 0, text.length))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ export function NativeMarkdownSelectableText(props: {
}
}

// Android renders the icon as an inline Image before the text. A regular space
// lets the line break between them, stranding the icon on the previous line.
if (Platform.OS === "android" && (run.fileIcon || linkIcon)) {
text = `\u00A0${text}`;
}

return { key: `${signature}:${occurrence}`, run, text, linkIcon };
});
// T3MarkdownText only rebuilds its attributed string during native layout. A
Expand Down
Loading