mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Move bidi methods to BidiUtil.
This commit is contained in:
@@ -16,7 +16,7 @@ import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -255,7 +255,7 @@ data class Boost(
|
||||
dend: Int
|
||||
): CharSequence? {
|
||||
val result = dest.subSequence(0, dstart).toString() + source.toString() + dest.subSequence(dend, dest.length)
|
||||
val resultWithoutCurrencyPrefix = StringUtil.stripBidiIndicator(result.removePrefix(symbol).removeSuffix(symbol).trim())
|
||||
val resultWithoutCurrencyPrefix = BidiUtil.stripBidiIndicator(result.removePrefix(symbol).removeSuffix(symbol).trim())
|
||||
|
||||
if (resultWithoutCurrencyPrefix.length == 1 && !resultWithoutCurrencyPrefix.isDigitsOnly() && resultWithoutCurrencyPrefix != separator.toString()) {
|
||||
return dest.subSequence(dstart, dend)
|
||||
|
||||
@@ -11,7 +11,7 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.money.PlatformCurrencyUtil
|
||||
@@ -158,7 +158,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
fun setCustomAmount(rawAmount: String) {
|
||||
val amount = StringUtil.stripBidiIndicator(rawAmount)
|
||||
val amount = BidiUtil.stripBidiIndicator(rawAmount)
|
||||
val bigDecimalAmount: BigDecimal = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
|
||||
@@ -70,6 +70,7 @@ import com.google.common.collect.Sets;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -561,7 +562,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
isFooterVisible(messageRecord, nextMessageRecord, groupThread) &&
|
||||
!bodyText.isJumbomoji() &&
|
||||
conversationMessage.getBottomButton() == null &&
|
||||
!StringUtil.hasMixedTextDirection(bodyText.getText()) &&
|
||||
!BidiUtil.hasMixedTextDirection(bodyText.getText()) &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
bodyText.getLastLineWidth() > 0)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.view.View
|
||||
import android.widget.Space
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
@@ -80,7 +80,7 @@ class V2FooterPositionDelegate private constructor(
|
||||
return false
|
||||
}
|
||||
|
||||
if (body.isJumbomoji || StringUtil.hasMixedTextDirection(body.text)) {
|
||||
if (body.isJumbomoji || BidiUtil.hasMixedTextDirection(body.text)) {
|
||||
displayUnderneathBody()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.core.util.isNullOrEmpty
|
||||
import org.signal.storageservice.protos.groups.AccessControl
|
||||
import org.signal.storageservice.protos.groups.AccessControl.AccessRequired
|
||||
@@ -340,7 +340,7 @@ object GroupsV2UpdateMessageConverter {
|
||||
fun translateNewTitle(change: DecryptedGroupChange, editorUnknown: Boolean, updates: MutableList<GroupChangeChatUpdate.Update>) {
|
||||
if (change.newTitle != null) {
|
||||
val editorAci = if (editorUnknown) null else change.editorServiceIdBytes
|
||||
val newTitle = StringUtil.isolateBidi(change.newTitle?.value_)
|
||||
val newTitle = BidiUtil.isolateBidi(change.newTitle?.value_)
|
||||
updates.add(
|
||||
GroupChangeChatUpdate.Update(
|
||||
groupNameUpdate = GroupNameUpdate(
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.StringRes;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
@@ -551,9 +552,9 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
private void describeGroupNameUpdate(@NonNull GroupNameUpdate update, @NonNull List<UpdateDescription> updates) {
|
||||
if (update.updaterAci == null) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(update.newGroupName)), R.drawable.ic_update_group_name_16));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, BidiUtil.isolateBidi(update.newGroupName)), R.drawable.ic_update_group_name_16));
|
||||
} else {
|
||||
String newTitle = StringUtil.isolateBidi(update.newGroupName);
|
||||
String newTitle = BidiUtil.isolateBidi(update.newGroupName);
|
||||
if (selfIds.matches(update.updaterAci)) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_16));
|
||||
} else {
|
||||
@@ -1081,7 +1082,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
boolean editorIsYou = selfIds.matches(change.editorServiceIdBytes);
|
||||
|
||||
if (change.newTitle != null) {
|
||||
String newTitle = StringUtil.isolateBidi(change.newTitle.value_);
|
||||
String newTitle = BidiUtil.isolateBidi(change.newTitle.value_);
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_16));
|
||||
} else {
|
||||
@@ -1104,7 +1105,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.newTitle != null) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.newTitle.value_)), R.drawable.ic_update_group_name_16));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, BidiUtil.isolateBidi(change.newTitle.value_)), R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,7 +1119,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_description, groupDescriptionUpdate.updaterAci, R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
} else {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(groupDescriptionUpdate.newDescription)), R.drawable.ic_update_group_name_16));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, BidiUtil.isolateBidi(groupDescriptionUpdate.newDescription)), R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.core.content.ContextCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
@@ -470,8 +471,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
if (profileChangeDetails != null) {
|
||||
if (profileChangeDetails.profileNameChange != null) {
|
||||
String displayName = getFromRecipient().getDisplayName(context);
|
||||
String newName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.profileNameChange.newValue).toString());
|
||||
String previousName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.profileNameChange.previous).toString());
|
||||
String newName = BidiUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.profileNameChange.newValue).toString());
|
||||
String previousName = BidiUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.profileNameChange.previous).toString());
|
||||
|
||||
String updateMessage;
|
||||
if (getFromRecipient().isSystemContact()) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
import com.google.i18n.phonenumbers.ShortNumberInfo;
|
||||
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
@@ -80,7 +81,7 @@ public class PhoneNumberFormatter {
|
||||
}
|
||||
|
||||
public static @NonNull String prettyPrint(@NonNull String e164) {
|
||||
return StringUtil.forceLtr(get(AppDependencies.getApplication()).prettyPrintFormat(e164));
|
||||
return BidiUtil.forceLtr(get(AppDependencies.getApplication()).prettyPrintFormat(e164));
|
||||
}
|
||||
|
||||
public @NonNull String prettyPrintFormat(@NonNull String e164) {
|
||||
@@ -91,13 +92,13 @@ public class PhoneNumberFormatter {
|
||||
localNumber.get().countryCode == parsedNumber.getCountryCode() &&
|
||||
NATIONAL_FORMAT_COUNTRY_CODES.contains(localNumber.get().getCountryCode()))
|
||||
{
|
||||
return StringUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL));
|
||||
return BidiUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL));
|
||||
} else {
|
||||
return StringUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
|
||||
return BidiUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
|
||||
}
|
||||
} catch (NumberParseException e) {
|
||||
Log.w(TAG, "Failed to format number: " + e.toString());
|
||||
return StringUtil.isolateBidi(e164);
|
||||
return BidiUtil.isolateBidi(e164);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
@@ -155,9 +156,9 @@ class EditProfileViewModel extends ViewModel {
|
||||
|
||||
repository.uploadProfile(profileName,
|
||||
displayName,
|
||||
!Objects.equals(StringUtil.stripBidiProtection(oldDisplayName), displayName),
|
||||
!Objects.equals(BidiUtil.stripBidiProtection(oldDisplayName), displayName),
|
||||
description,
|
||||
!Objects.equals(StringUtil.stripBidiProtection(oldDescription), description),
|
||||
!Objects.equals(BidiUtil.stripBidiProtection(oldDescription), description),
|
||||
newAvatar,
|
||||
!Arrays.equals(oldAvatar, newAvatar),
|
||||
uploadResult::postValue);
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.nullIfBlank
|
||||
@@ -532,7 +532,7 @@ class Recipient(
|
||||
if (Util.isEmpty(name)) {
|
||||
name = getUnknownDisplayName(context)
|
||||
}
|
||||
return StringUtil.isolateBidi(name)
|
||||
return BidiUtil.isolateBidi(name)
|
||||
}
|
||||
|
||||
fun hasNonUsernameDisplayName(context: Context): Boolean {
|
||||
@@ -569,21 +569,21 @@ class Recipient(
|
||||
/** A display name to use when rendering a mention of this user. */
|
||||
fun getMentionDisplayName(context: Context): String {
|
||||
var name: String? = if (isSelf) profileName.toString() else getGroupName(context)
|
||||
name = StringUtil.isolateBidi(name)
|
||||
name = BidiUtil.isolateBidi(name)
|
||||
|
||||
if (name.isBlank()) {
|
||||
name = if (isSelf) getGroupName(context) else nickname.toString()
|
||||
name = StringUtil.isolateBidi(name)
|
||||
name = BidiUtil.isolateBidi(name)
|
||||
}
|
||||
|
||||
if (name.isBlank()) {
|
||||
name = if (isSelf) getGroupName(context) else systemContactName
|
||||
name = StringUtil.isolateBidi(name)
|
||||
name = BidiUtil.isolateBidi(name)
|
||||
}
|
||||
|
||||
if (name.isBlank()) {
|
||||
name = if (isSelf) getGroupName(context) else profileName.toString()
|
||||
name = StringUtil.isolateBidi(name)
|
||||
name = BidiUtil.isolateBidi(name)
|
||||
}
|
||||
|
||||
if (name.isBlank() && e164Value.isNotNullOrBlank()) {
|
||||
@@ -591,11 +591,11 @@ class Recipient(
|
||||
}
|
||||
|
||||
if (name.isBlank()) {
|
||||
name = StringUtil.isolateBidi(emailValue)
|
||||
name = BidiUtil.isolateBidi(emailValue)
|
||||
}
|
||||
|
||||
if (name.isBlank()) {
|
||||
name = StringUtil.isolateBidi(context.getString(R.string.Recipient_unknown))
|
||||
name = BidiUtil.isolateBidi(context.getString(R.string.Recipient_unknown))
|
||||
}
|
||||
|
||||
return name
|
||||
@@ -615,7 +615,7 @@ class Recipient(
|
||||
getDisplayName(context)
|
||||
).firstOrNull { it.isNotNullOrBlank() }
|
||||
|
||||
return StringUtil.isolateBidi(name)
|
||||
return BidiUtil.isolateBidi(name)
|
||||
}
|
||||
|
||||
private fun getUnknownDisplayName(context: Context): String {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.BidiUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
@@ -115,7 +116,7 @@ public final class GroupUtil {
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
String title = StringUtil.isolateBidi(groupContext.getName());
|
||||
String title = BidiUtil.isolateBidi(groupContext.getName());
|
||||
|
||||
if (members != null && members.size() > 0) {
|
||||
description.append("\n");
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.BidiUtil
|
||||
import org.signal.storageservice.protos.groups.AccessControl
|
||||
import org.signal.storageservice.protos.groups.AccessControl.AccessRequired
|
||||
import org.signal.storageservice.protos.groups.AccessControl.AccessRequired.ADMINISTRATOR
|
||||
@@ -683,7 +683,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
.title("New title")
|
||||
.build()
|
||||
|
||||
assertEquals(listOf("Alice changed the group name to \"" + StringUtil.isolateBidi("New title") + "\"."), describeChange(change))
|
||||
assertEquals(listOf("Alice changed the group name to \"" + BidiUtil.isolateBidi("New title") + "\"."), describeChange(change))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -692,7 +692,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
.title("Title 2")
|
||||
.build()
|
||||
|
||||
assertEquals(listOf("You changed the group name to \"" + StringUtil.isolateBidi("Title 2") + "\"."), describeChange(change))
|
||||
assertEquals(listOf("You changed the group name to \"" + BidiUtil.isolateBidi("Title 2") + "\"."), describeChange(change))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -701,7 +701,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
.title("Title 3")
|
||||
.build()
|
||||
|
||||
assertEquals(listOf("The group name has changed to \"" + StringUtil.isolateBidi("Title 3") + "\"."), describeChange(change))
|
||||
assertEquals(listOf("The group name has changed to \"" + BidiUtil.isolateBidi("Title 3") + "\"."), describeChange(change))
|
||||
}
|
||||
|
||||
// Avatar change
|
||||
@@ -1166,7 +1166,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
listOf(
|
||||
"Alice added you to the group.",
|
||||
"Alice added Bob.",
|
||||
"Alice changed the group name to \"" + StringUtil.isolateBidi("Title") + "\".",
|
||||
"Alice changed the group name to \"" + BidiUtil.isolateBidi("Title") + "\".",
|
||||
"Alice set the disappearing message timer to 5 minutes.",
|
||||
"Alice changed who can edit group membership to \"All members\"."
|
||||
),
|
||||
@@ -1219,7 +1219,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
assertEquals(
|
||||
listOf(
|
||||
"Bob joined the group.",
|
||||
"The group name has changed to \"" + StringUtil.isolateBidi("Title 2") + "\".",
|
||||
"The group name has changed to \"" + BidiUtil.isolateBidi("Title 2") + "\".",
|
||||
"The group avatar has been changed.",
|
||||
"The disappearing message timer has been set to 10 minutes.",
|
||||
"Who can edit group membership has been changed to \"All members\"."
|
||||
@@ -1241,7 +1241,7 @@ class GroupsV2UpdateMessageProducerTest {
|
||||
listOf(
|
||||
"Alice joined the group.",
|
||||
"Alice is now an admin.",
|
||||
"The group name has changed to \"" + StringUtil.isolateBidi("Updated title") + "\".",
|
||||
"The group name has changed to \"" + BidiUtil.isolateBidi("Updated title") + "\".",
|
||||
"Alice is no longer in the group."
|
||||
),
|
||||
describeChange(change)
|
||||
|
||||
155
core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt
Normal file
155
core-util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object BidiUtil {
|
||||
private val ALL_ASCII_PATTERN: Pattern = Pattern.compile("^[\\x00-\\x7F]*$")
|
||||
|
||||
private object Bidi {
|
||||
/** Override text direction */
|
||||
val OVERRIDES: Set<Int> = SetUtil.newHashSet(
|
||||
"\u202a".codePointAt(0), // LRE
|
||||
"\u202b".codePointAt(0), // RLE
|
||||
"\u202d".codePointAt(0), // LRO
|
||||
"\u202e".codePointAt(0) // RLO
|
||||
)
|
||||
|
||||
/** Set direction and isolate surrounding text */
|
||||
val ISOLATES: Set<Int> = SetUtil.newHashSet(
|
||||
"\u2066".codePointAt(0), // LRI
|
||||
"\u2067".codePointAt(0), // RLI
|
||||
"\u2068".codePointAt(0) // FSI
|
||||
)
|
||||
|
||||
/** Closes things in [.OVERRIDES] */
|
||||
val PDF: Int = "\u202c".codePointAt(0)
|
||||
|
||||
/** Closes things in [.ISOLATES] */
|
||||
val PDI: Int = "\u2069".codePointAt(0)
|
||||
|
||||
/** Auto-detecting isolate */
|
||||
val FSI: Int = "\u2068".codePointAt(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the provided text contains a mix of LTR and RTL characters, otherwise false.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasMixedTextDirection(text: CharSequence?): Boolean {
|
||||
if (text == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
var isLtr: Boolean? = null
|
||||
|
||||
var i = 0
|
||||
val len = Character.codePointCount(text, 0, text.length)
|
||||
while (i < len) {
|
||||
val codePoint = Character.codePointAt(text, i)
|
||||
val direction = Character.getDirectionality(codePoint)
|
||||
val isLetter = Character.isLetter(codePoint)
|
||||
|
||||
if (isLtr != null && isLtr && direction != Character.DIRECTIONALITY_LEFT_TO_RIGHT && isLetter) {
|
||||
return true
|
||||
} else if (isLtr != null && !isLtr && direction != Character.DIRECTIONALITY_RIGHT_TO_LEFT && isLetter) {
|
||||
return true
|
||||
} else if (isLetter) {
|
||||
isLtr = direction == Character.DIRECTIONALITY_LEFT_TO_RIGHT
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolates bi-directional text from influencing surrounding text. You should use this whenever
|
||||
* you're injecting user-generated text into a larger string.
|
||||
*
|
||||
* You'd think we'd be able to trust BidiFormatter, but unfortunately it just misses some
|
||||
* corner cases, so here we are.
|
||||
*
|
||||
* The general idea is just to balance out the opening and closing codepoints, and then wrap the
|
||||
* whole thing in FSI/PDI to isolate it.
|
||||
*
|
||||
* For more details, see:
|
||||
* https://www.w3.org/International/questions/qa-bidi-unicode-controls
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isolateBidi(text: String?): String {
|
||||
if (text == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (text.isEmpty()) {
|
||||
return text
|
||||
}
|
||||
|
||||
if (ALL_ASCII_PATTERN.matcher(text).matches()) {
|
||||
return text
|
||||
}
|
||||
|
||||
var overrideCount = 0
|
||||
var overrideCloseCount = 0
|
||||
var isolateCount = 0
|
||||
var isolateCloseCount = 0
|
||||
|
||||
var i = 0
|
||||
val len = text.codePointCount(0, text.length)
|
||||
while (i < len) {
|
||||
val codePoint = text.codePointAt(i)
|
||||
|
||||
if (Bidi.OVERRIDES.contains(codePoint)) {
|
||||
overrideCount++
|
||||
} else if (codePoint == Bidi.PDF) {
|
||||
overrideCloseCount++
|
||||
} else if (Bidi.ISOLATES.contains(codePoint)) {
|
||||
isolateCount++
|
||||
} else if (codePoint == Bidi.PDI) {
|
||||
isolateCloseCount++
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
val suffix = StringBuilder()
|
||||
|
||||
while (overrideCount > overrideCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.PDF)
|
||||
overrideCloseCount++
|
||||
}
|
||||
|
||||
while (isolateCount > isolateCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.FSI)
|
||||
isolateCloseCount++
|
||||
}
|
||||
|
||||
val out = StringBuilder()
|
||||
|
||||
return out.appendCodePoint(Bidi.FSI)
|
||||
.append(text)
|
||||
.append(suffix)
|
||||
.appendCodePoint(Bidi.PDI)
|
||||
.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun stripBidiProtection(text: String?): String? {
|
||||
if (text == null) return null
|
||||
|
||||
return text.replace("[\\u2068\\u2069\\u202c]".toRegex(), "")
|
||||
}
|
||||
|
||||
fun stripBidiIndicator(text: String): String {
|
||||
return text.replace("\u200F", "")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forceLtr(text: CharSequence): String {
|
||||
return "\u202a" + text + "\u202c"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.text.SpannableStringBuilder
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object StringUtil {
|
||||
private val WHITESPACE: Set<Char> = setOf(
|
||||
@@ -15,34 +14,6 @@ object StringUtil {
|
||||
'\u2800' // braille blank
|
||||
)
|
||||
|
||||
private val ALL_ASCII_PATTERN: Pattern = Pattern.compile("^[\\x00-\\x7F]*$")
|
||||
|
||||
private object Bidi {
|
||||
/** Override text direction */
|
||||
val OVERRIDES: Set<Int> = SetUtil.newHashSet(
|
||||
"\u202a".codePointAt(0), /* LRE */
|
||||
"\u202b".codePointAt(0), /* RLE */
|
||||
"\u202d".codePointAt(0), /* LRO */
|
||||
"\u202e".codePointAt(0) /* RLO */
|
||||
)
|
||||
|
||||
/** Set direction and isolate surrounding text */
|
||||
val ISOLATES: Set<Int> = SetUtil.newHashSet(
|
||||
"\u2066".codePointAt(0), /* LRI */
|
||||
"\u2067".codePointAt(0), /* RLI */
|
||||
"\u2068".codePointAt(0) /* FSI */
|
||||
)
|
||||
|
||||
/** Closes things in [.OVERRIDES] */
|
||||
val PDF: Int = "\u202c".codePointAt(0)
|
||||
|
||||
/** Closes things in [.ISOLATES] */
|
||||
val PDI: Int = "\u2069".codePointAt(0)
|
||||
|
||||
/** Auto-detecting isolate */
|
||||
val FSI: Int = "\u2068".codePointAt(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a name string to fit into the byte length requirement.
|
||||
*
|
||||
@@ -172,37 +143,6 @@ object StringUtil {
|
||||
return String(Character.toChars(codePoint))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the provided text contains a mix of LTR and RTL characters, otherwise false.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun hasMixedTextDirection(text: CharSequence?): Boolean {
|
||||
if (text == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
var isLtr: Boolean? = null
|
||||
|
||||
var i = 0
|
||||
val len = Character.codePointCount(text, 0, text.length)
|
||||
while (i < len) {
|
||||
val codePoint = Character.codePointAt(text, i)
|
||||
val direction = Character.getDirectionality(codePoint)
|
||||
val isLetter = Character.isLetter(codePoint)
|
||||
|
||||
if (isLtr != null && isLtr && direction != Character.DIRECTIONALITY_LEFT_TO_RIGHT && isLetter) {
|
||||
return true
|
||||
} else if (isLtr != null && !isLtr && direction != Character.DIRECTIONALITY_RIGHT_TO_LEFT && isLetter) {
|
||||
return true
|
||||
} else if (isLetter) {
|
||||
isLtr = direction == Character.DIRECTIONALITY_LEFT_TO_RIGHT
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the text is null or has a length of 0, otherwise false.
|
||||
*/
|
||||
@@ -211,87 +151,6 @@ object StringUtil {
|
||||
return text.isNullOrEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolates bi-directional text from influencing surrounding text. You should use this whenever
|
||||
* you're injecting user-generated text into a larger string.
|
||||
*
|
||||
* You'd think we'd be able to trust BidiFormatter, but unfortunately it just misses some
|
||||
* corner cases, so here we are.
|
||||
*
|
||||
* The general idea is just to balance out the opening and closing codepoints, and then wrap the
|
||||
* whole thing in FSI/PDI to isolate it.
|
||||
*
|
||||
* For more details, see:
|
||||
* https://www.w3.org/International/questions/qa-bidi-unicode-controls
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isolateBidi(text: String?): String {
|
||||
if (text == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (isEmpty(text)) {
|
||||
return text
|
||||
}
|
||||
|
||||
if (ALL_ASCII_PATTERN.matcher(text).matches()) {
|
||||
return text
|
||||
}
|
||||
|
||||
var overrideCount = 0
|
||||
var overrideCloseCount = 0
|
||||
var isolateCount = 0
|
||||
var isolateCloseCount = 0
|
||||
|
||||
var i = 0
|
||||
val len = text.codePointCount(0, text.length)
|
||||
while (i < len) {
|
||||
val codePoint = text.codePointAt(i)
|
||||
|
||||
if (Bidi.OVERRIDES.contains(codePoint)) {
|
||||
overrideCount++
|
||||
} else if (codePoint == Bidi.PDF) {
|
||||
overrideCloseCount++
|
||||
} else if (Bidi.ISOLATES.contains(codePoint)) {
|
||||
isolateCount++
|
||||
} else if (codePoint == Bidi.PDI) {
|
||||
isolateCloseCount++
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
val suffix = StringBuilder()
|
||||
|
||||
while (overrideCount > overrideCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.PDF)
|
||||
overrideCloseCount++
|
||||
}
|
||||
|
||||
while (isolateCount > isolateCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.FSI)
|
||||
isolateCloseCount++
|
||||
}
|
||||
|
||||
val out = StringBuilder()
|
||||
|
||||
return out.appendCodePoint(Bidi.FSI)
|
||||
.append(text)
|
||||
.append(suffix)
|
||||
.appendCodePoint(Bidi.PDI)
|
||||
.toString()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun stripBidiProtection(text: String?): String? {
|
||||
if (text == null) return null
|
||||
|
||||
return text.replace("[\\u2068\\u2069\\u202c]".toRegex(), "")
|
||||
}
|
||||
|
||||
fun stripBidiIndicator(text: String): String {
|
||||
return text.replace("\u200F", "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a [CharSequence] of starting and trailing whitespace. Behavior matches
|
||||
* [String.trim] to preserve expectations around results.
|
||||
@@ -341,11 +200,6 @@ object StringUtil {
|
||||
return iterator.countBreaks()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forceLtr(text: CharSequence): String {
|
||||
return "\u202a" + text + "\u202c"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun replace(text: CharSequence, toReplace: Char, replacement: String?): CharSequence {
|
||||
var updatedText: SpannableStringBuilder? = null
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class StringUtilTest_hasMixedTextDirection {
|
||||
|
||||
@Test
|
||||
public void trim() {
|
||||
boolean output = StringUtil.hasMixedTextDirection(input);
|
||||
boolean output = BidiUtil.hasMixedTextDirection(input);
|
||||
assertEquals(expected, output);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user