feat(chat): support dynamic BiDi Arabic input and isolated mixed text in Markdown (#1047)

This commit is contained in:
M57 authored and GitHub committed 2026-09-03 13:06:03 +03:00
1 parent fa17c95296
commit cd8c27e62b
8 files changed
+868 -401

No files matched your search

@@ -11,6 +11,7 @@ import com.m57.hermescontrol.theme.StatusRedContainer
import com.m57.hermescontrol.theme.StatusYellow
import com.m57.hermescontrol.theme.StatusYellowContainer
import com.m57.hermescontrol.theme.searchHighlightColors
import com.m57.hermescontrol.util.BidiUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -538,4 +539,28 @@ class MarkdownTextFeatureTest {
val codeBlock = blocks.single() as MdBlock.Code
assertTrue(codeBlock.code.contains("```python"))
}
// 25. ARABIC BIDI MIXED TEXT (issue #1044)
@Test
fun testArabicMixedWithInlineCode_wrapsWithLtrIsolate() {
val input = "تم اختبار كود `val x = 1` بنجاح"
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
val expected = "تم اختبار كود ${BidiUtils.LRI}val x = 1${BidiUtils.PDI} بنجاح"
assertEquals(expected, parsed.toString())
}
@Test
fun testArabicMixedWithBoldEnglish_wrapsWithLtrIsolate() {
val input = "هذا النص يحتوي على **Android** داخل فقرة"
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
val expected = "هذا النص يحتوي على ${BidiUtils.LRI}Android${BidiUtils.PDI} داخل فقرة"
assertEquals(expected, parsed.toString())
}
@Test
fun testEnglishWithInlineCode_doesNotWrapWithIsolate() {
val input = "Run `val x = 1` here"
val parsed = parseInline(input, Color.Black, "", false, Color.Blue, DEFAULT_HIGHLIGHTS)
assertEquals("Run val x = 1 here", parsed.toString())
}
}
@@ -0,0 +1,89 @@
package com.m57.hermescontrol.util
import androidx.compose.ui.unit.LayoutDirection
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class BidiUtilsTest {
@Test
fun testIsRtlText_pureArabic() {
assertTrue(BidiUtils.isRtlText("مرحبا بك في تطبيق هيرمس"))
}
@Test
fun testIsRtlText_pureEnglish() {
assertFalse(BidiUtils.isRtlText("Hello from Hermes Control"))
}
@Test
fun testIsRtlText_mixedArabicWithEnglishWords() {
val text = "هذا النص يحتوي على Android و Compose داخل فقرة عربية"
assertTrue(BidiUtils.isRtlText(text))
}
@Test
fun testIsRtlText_arabicStartingWithEnglishTerm() {
val text = "Android هو نظام تشغيل رائع ومميز جدا"
assertTrue(BidiUtils.isRtlText(text))
}
@Test
fun testIsRtlText_englishSentenceWithArabicWord() {
val text = "The Arabic word for welcome is مرحبا in most dialects"
assertFalse(BidiUtils.isRtlText(text))
}
@Test
fun testIsRtlText_neutralOnly() {
assertFalse(BidiUtils.isRtlText(""))
assertFalse(BidiUtils.isRtlText(" "))
assertFalse(BidiUtils.isRtlText("12345"))
assertFalse(BidiUtils.isRtlText("🚀✨🔥"))
assertFalse(BidiUtils.isRtlText("/status"))
}
@Test
fun testIsRtlText_neutralPrefixWithArabic() {
assertTrue(BidiUtils.isRtlText("🚀 مرحبا يا صديقي"))
assertTrue(BidiUtils.isRtlText("- [x] مهمة جديدة مع Compose"))
}
@Test
fun testDetectTextDirection() {
assertEquals(BidiUtils.BidiDirection.RTL, BidiUtils.detectTextDirection("أهلا"))
assertEquals(BidiUtils.BidiDirection.LTR, BidiUtils.detectTextDirection("Hello"))
assertEquals(BidiUtils.BidiDirection.NEUTRAL, BidiUtils.detectTextDirection("123 🚀"))
}
@Test
fun testResolveLayoutDirection() {
assertEquals(LayoutDirection.Rtl, BidiUtils.resolveLayoutDirection("صباح الخير"))
assertEquals(LayoutDirection.Ltr, BidiUtils.resolveLayoutDirection("Good morning"))
assertEquals(LayoutDirection.Ltr, BidiUtils.resolveLayoutDirection("123", fallback = LayoutDirection.Ltr))
assertEquals(LayoutDirection.Rtl, BidiUtils.resolveLayoutDirection("123", fallback = LayoutDirection.Rtl))
}
@Test
fun testWrapLtrIsolate() {
assertEquals("${BidiUtils.LRI}Android${BidiUtils.PDI}", BidiUtils.wrapLtrIsolate("Android"))
assertEquals("${BidiUtils.LRI}val x = 1${BidiUtils.PDI}", BidiUtils.wrapLtrIsolate("val x = 1"))
// RTL snippet should not be wrapped
assertEquals("مرحبا", BidiUtils.wrapLtrIsolate("مرحبا"))
}
@Test
fun testAnchorTrailingRtl() {
val rtlWithEmoji = "مرحبا بكم 🚀"
assertEquals("مرحبا بكم 🚀${BidiUtils.RLM}", BidiUtils.anchorTrailingRtl(rtlWithEmoji))
// Does not append duplicate RLM
val alreadyAnchored = "مرحبا بكم 🚀${BidiUtils.RLM}"
assertEquals(alreadyAnchored, BidiUtils.anchorTrailingRtl(alreadyAnchored))
// LTR text remains unmodified
val ltrWithEmoji = "Welcome to Hermes 🚀"
assertEquals(ltrWithEmoji, BidiUtils.anchorTrailingRtl(ltrWithEmoji))
}
}