Merge pull request #1065 from Hy4ri/fix/bidi-arabic-english-word-isolation
fix(bidi): isolate leading and inline English words in RTL paragraphs
This commit is contained in:
4 files changed
+50
-15
No files matched your search
@@ -292,9 +292,9 @@ fun GroupChatScreen(
|
|||||||
MaterialTheme.typography.bodySmall.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isThinkingRtl) {
|
if (isThinkingRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.ContentOrLtr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
@@ -914,9 +914,9 @@ private fun GroupMessageCard(
|
|||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isSystemRtl) {
|
if (isSystemRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.ContentOrLtr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -946,9 +946,9 @@ private fun GroupMessageCard(
|
|||||||
MaterialTheme.typography.bodyMedium.copy(
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isUserRtl) {
|
if (isUserRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.ContentOrLtr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
@@ -1017,9 +1017,9 @@ private fun GroupMessageCard(
|
|||||||
MaterialTheme.typography.labelMedium.copy(
|
MaterialTheme.typography.labelMedium.copy(
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isBotNameRtl) {
|
if (isBotNameRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.ContentOrLtr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
@@ -1168,7 +1168,7 @@ private fun GroupChatToolChip(
|
|||||||
text = label,
|
text = label,
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.labelSmall.copy(
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
textDirection = if (isLabelRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr,
|
textDirection = if (isLabelRtl) TextDirection.Rtl else TextDirection.Ltr,
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -1246,7 +1246,7 @@ private fun GroupChatToolChip(
|
|||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isOutputRtl) {
|
if (isOutputRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.Ltr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -197,9 +197,9 @@ fun ChatBubble(
|
|||||||
MaterialTheme.typography.bodyMedium.copy(
|
MaterialTheme.typography.bodyMedium.copy(
|
||||||
textDirection =
|
textDirection =
|
||||||
if (isRtl) {
|
if (isRtl) {
|
||||||
TextDirection.ContentOrRtl
|
TextDirection.Rtl
|
||||||
} else {
|
} else {
|
||||||
TextDirection.ContentOrLtr
|
TextDirection.Ltr
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ private val BULLET_LINE_RE = Regex("""^(\s*)[-*+]\s+(.*)$""")
|
|||||||
private val ORDERED_LINE_RE = Regex("""^(\s*)(\d+)\.\s+(.*)$""")
|
private val ORDERED_LINE_RE = Regex("""^(\s*)(\d+)\.\s+(.*)$""")
|
||||||
private val LIST_ITEM_LINE_RE = Regex("""^(\s*)(?:[-*+]\s+(?:\[([ xX])\]\s+)?|(\d+)\.\s+)(.*)$""")
|
private val LIST_ITEM_LINE_RE = Regex("""^(\s*)(?:[-*+]\s+(?:\[([ xX])\]\s+)?|(\d+)\.\s+)(.*)$""")
|
||||||
|
|
||||||
private fun bidiTextDirection(isRtl: Boolean): TextDirection =
|
private fun bidiTextDirection(isRtl: Boolean): TextDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr
|
||||||
if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders chat assistant text as Markdown — but ONLY once the message has finished streaming.
|
* Renders chat assistant text as Markdown — but ONLY once the message has finished streaming.
|
||||||
@@ -551,7 +550,7 @@ private fun MarkdownInlineText(
|
|||||||
val direction = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
val direction = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||||
val resolvedStyle =
|
val resolvedStyle =
|
||||||
style.copy(
|
style.copy(
|
||||||
textDirection = if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr,
|
textDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr,
|
||||||
)
|
)
|
||||||
val processedText =
|
val processedText =
|
||||||
remember(text, isRtl) {
|
remember(text, isRtl) {
|
||||||
@@ -1499,6 +1498,34 @@ private fun parseInlineSource(
|
|||||||
i = match.range.last + 1
|
i = match.range.last + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plain text / words: when inside RTL text, wrap sequences of Latin/LTR words in LTR isolate
|
||||||
|
// so an English word at the start/middle of an Arabic line doesn't flip the layout direction.
|
||||||
|
isRtl && Character.isLetterOrDigit(src.codePointAt(i)) &&
|
||||||
|
Character.getDirectionality(src.codePointAt(i)) == Character.DIRECTIONALITY_LEFT_TO_RIGHT -> {
|
||||||
|
var end = i + Character.charCount(src.codePointAt(i))
|
||||||
|
while (end < src.length) {
|
||||||
|
val cp = src.codePointAt(end)
|
||||||
|
val dir = Character.getDirectionality(cp)
|
||||||
|
if (dir == Character.DIRECTIONALITY_LEFT_TO_RIGHT ||
|
||||||
|
dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER ||
|
||||||
|
dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR ||
|
||||||
|
dir == Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR ||
|
||||||
|
(
|
||||||
|
dir == Character.DIRECTIONALITY_WHITESPACE && end + 1 < src.length &&
|
||||||
|
Character.getDirectionality(src.codePointAt(end + 1)) ==
|
||||||
|
Character.DIRECTIONALITY_LEFT_TO_RIGHT
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
end += Character.charCount(cp)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ltrSnippet = src.substring(i, end)
|
||||||
|
append(BidiUtils.wrapLtrIsolate(ltrSnippet))
|
||||||
|
i = end
|
||||||
|
}
|
||||||
|
|
||||||
// search highlight
|
// search highlight
|
||||||
searchQuery.isNotEmpty() &&
|
searchQuery.isNotEmpty() &&
|
||||||
src.regionMatches(i, searchQuery, 0, searchQuery.length, ignoreCase = true) -> {
|
src.regionMatches(i, searchQuery, 0, searchQuery.length, ignoreCase = true) -> {
|
||||||
|
|||||||
@@ -563,4 +563,12 @@ class MarkdownTextFeatureTest {
|
|||||||
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
|
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
|
||||||
assertEquals("Run val x = 1 here", parsed.toString())
|
assertEquals("Run val x = 1 here", parsed.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testArabicStartingWithEnglishWord_wrapsEnglishInLtrIsolate() {
|
||||||
|
val input = "Okay سكرت كلشي وصار كامل عربي متل ما بدك يا بوبو"
|
||||||
|
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
|
||||||
|
val expected = "${BidiUtils.LRI}Okay${BidiUtils.PDI} سكرت كلشي وصار كامل عربي متل ما بدك يا بوبو"
|
||||||
|
assertEquals(expected, parsed.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user