fix(bidi): isolate leading and inline English words in RTL paragraphs
- In parseInlineSource, wrap consecutive Latin/LTR tokens in LTR isolates (LRI...PDI) when rendering inside an RTL block, preventing leading English terms like "Okay" from flipping paragraph layout direction and causing BiDi line shuffling. - Switch textDirection from TextDirection.ContentOrRtl to explicit TextDirection.Rtl when isRtl is true across MarkdownText, ChatBubble, and GroupChatScreen, preventing first-strong character fallbacks from overriding resolved paragraph direction.
This commit is contained in:
1 parent
199771b798
commit
5ce01e429b
4 files changed
+50
-15
No files matched your search
@@ -292,9 +292,9 @@ fun GroupChatScreen(
|
||||
MaterialTheme.typography.bodySmall.copy(
|
||||
textDirection =
|
||||
if (isThinkingRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} else {
|
||||
TextDirection.ContentOrLtr
|
||||
TextDirection.Ltr
|
||||
},
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
@@ -914,9 +914,9 @@ private fun GroupMessageCard(
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
textDirection =
|
||||
if (isSystemRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} else {
|
||||
TextDirection.ContentOrLtr
|
||||
TextDirection.Ltr
|
||||
},
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -946,9 +946,9 @@ private fun GroupMessageCard(
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection =
|
||||
if (isUserRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} else {
|
||||
TextDirection.ContentOrLtr
|
||||
TextDirection.Ltr
|
||||
},
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
@@ -1017,9 +1017,9 @@ private fun GroupMessageCard(
|
||||
MaterialTheme.typography.labelMedium.copy(
|
||||
textDirection =
|
||||
if (isBotNameRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} else {
|
||||
TextDirection.ContentOrLtr
|
||||
TextDirection.Ltr
|
||||
},
|
||||
),
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -1168,7 +1168,7 @@ private fun GroupChatToolChip(
|
||||
text = label,
|
||||
style =
|
||||
MaterialTheme.typography.labelSmall.copy(
|
||||
textDirection = if (isLabelRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr,
|
||||
textDirection = if (isLabelRtl) TextDirection.Rtl else TextDirection.Ltr,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
@@ -1246,7 +1246,7 @@ private fun GroupChatToolChip(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
textDirection =
|
||||
if (isOutputRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} else {
|
||||
TextDirection.Ltr
|
||||
},
|
||||
|
||||
@@ -197,9 +197,9 @@ fun ChatBubble(
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection =
|
||||
if (isRtl) {
|
||||
TextDirection.ContentOrRtl
|
||||
TextDirection.Rtl
|
||||
} 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 LIST_ITEM_LINE_RE = Regex("""^(\s*)(?:[-*+]\s+(?:\[([ xX])\]\s+)?|(\d+)\.\s+)(.*)$""")
|
||||
|
||||
private fun bidiTextDirection(isRtl: Boolean): TextDirection =
|
||||
if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr
|
||||
private fun bidiTextDirection(isRtl: Boolean): TextDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr
|
||||
|
||||
/**
|
||||
* 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 resolvedStyle =
|
||||
style.copy(
|
||||
textDirection = if (isRtl) TextDirection.ContentOrRtl else TextDirection.ContentOrLtr,
|
||||
textDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr,
|
||||
)
|
||||
val processedText =
|
||||
remember(text, isRtl) {
|
||||
@@ -1499,6 +1498,34 @@ private fun parseInlineSource(
|
||||
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
|
||||
searchQuery.isNotEmpty() &&
|
||||
src.regionMatches(i, searchQuery, 0, searchQuery.length, ignoreCase = true) -> {
|
||||
|
||||
Reference in new issue
Block a user