C#:
// Requires: BetterChat
using System;
using System.Collections.Generic;
using System.Text;
namespace Oxide.Plugins
{
[Info("Arabic Chat Shaper", "nostalgik", "1.0.0")]
[Description("Shapes and reorders Arabic-script chat messages for correct display with Better Chat.")]
public class ArabicChatShaper : CovalencePlugin
{
private sealed class ArabicForm
{
public readonly char Isolated;
public readonly char Final;
public readonly char Initial;
public readonly char Medial;
public readonly bool ConnectsToPrevious;
public readonly bool ConnectsToNext;
public ArabicForm(
char isolated,
char finalForm,
char initial,
char medial,
bool connectsToPrevious,
bool connectsToNext)
{
Isolated = isolated;
Final = finalForm;
Initial = initial;
Medial = medial;
ConnectsToPrevious = connectsToPrevious;
ConnectsToNext = connectsToNext;
}
}
private static readonly Dictionary<char, ArabicForm> Forms =
new Dictionary<char, ArabicForm>
{
['ء'] = new ArabicForm('ﺀ', '\0', '\0', '\0', false, false), // ء
['آ'] = new ArabicForm('ﺁ', 'ﺂ', '\0', '\0', true, false), // آ
['أ'] = new ArabicForm('ﺃ', 'ﺄ', '\0', '\0', true, false), // أ
['ؤ'] = new ArabicForm('ﺅ', 'ﺆ', '\0', '\0', true, false), // ؤ
['إ'] = new ArabicForm('ﺇ', 'ﺈ', '\0', '\0', true, false), // إ
['ئ'] = new ArabicForm('ﺉ', 'ﺊ', 'ﺋ', 'ﺌ', true, true), // ئ
['ا'] = new ArabicForm('ﺍ', 'ﺎ', '\0', '\0', true, false), // ا
['ب'] = new ArabicForm('ﺏ', 'ﺐ', 'ﺑ', 'ﺒ', true, true), // ب
['ة'] = new ArabicForm('ﺓ', 'ﺔ', '\0', '\0', true, false), // ة
['ت'] = new ArabicForm('ﺕ', 'ﺖ', 'ﺗ', 'ﺘ', true, true), // ت
['ث'] = new ArabicForm('ﺙ', 'ﺚ', 'ﺛ', 'ﺜ', true, true), // ث
['ج'] = new ArabicForm('ﺝ', 'ﺞ', 'ﺟ', 'ﺠ', true, true), // ج
['ح'] = new ArabicForm('ﺡ', 'ﺢ', 'ﺣ', 'ﺤ', true, true), // ح
['خ'] = new ArabicForm('ﺥ', 'ﺦ', 'ﺧ', 'ﺨ', true, true), // خ
['د'] = new ArabicForm('ﺩ', 'ﺪ', '\0', '\0', true, false), // د
['ذ'] = new ArabicForm('ﺫ', 'ﺬ', '\0', '\0', true, false), // ذ
['ر'] = new ArabicForm('ﺭ', 'ﺮ', '\0', '\0', true, false), // ر
['ز'] = new ArabicForm('ﺯ', 'ﺰ', '\0', '\0', true, false), // ز
['س'] = new ArabicForm('ﺱ', 'ﺲ', 'ﺳ', 'ﺴ', true, true), // س
['ش'] = new ArabicForm('ﺵ', 'ﺶ', 'ﺷ', 'ﺸ', true, true), // ش
['ص'] = new ArabicForm('ﺹ', 'ﺺ', 'ﺻ', 'ﺼ', true, true), // ص
['ض'] = new ArabicForm('ﺽ', 'ﺾ', 'ﺿ', 'ﻀ', true, true), // ض
['ط'] = new ArabicForm('ﻁ', 'ﻂ', 'ﻃ', 'ﻄ', true, true), // ط
['ظ'] = new ArabicForm('ﻅ', 'ﻆ', 'ﻇ', 'ﻈ', true, true), // ظ
['ع'] = new ArabicForm('ﻉ', 'ﻊ', 'ﻋ', 'ﻌ', true, true), // ع
['غ'] = new ArabicForm('ﻍ', 'ﻎ', 'ﻏ', 'ﻐ', true, true), // غ
['ف'] = new ArabicForm('ﻑ', 'ﻒ', 'ﻓ', 'ﻔ', true, true), // ف
['ق'] = new ArabicForm('ﻕ', 'ﻖ', 'ﻗ', 'ﻘ', true, true), // ق
['ك'] = new ArabicForm('ﻙ', 'ﻚ', 'ﻛ', 'ﻜ', true, true), // ك
['ل'] = new ArabicForm('ﻝ', 'ﻞ', 'ﻟ', 'ﻠ', true, true), // ل
['م'] = new ArabicForm('ﻡ', 'ﻢ', 'ﻣ', 'ﻤ', true, true), // م
['ن'] = new ArabicForm('ﻥ', 'ﻦ', 'ﻧ', 'ﻨ', true, true), // ن
['ه'] = new ArabicForm('ﻩ', 'ﻪ', 'ﻫ', 'ﻬ', true, true), // ه
['و'] = new ArabicForm('ﻭ', 'ﻮ', '\0', '\0', true, false), // و
['ى'] = new ArabicForm('ﻯ', 'ﻰ', '\0', '\0', true, false), // ى
['ي'] = new ArabicForm('ﻱ', 'ﻲ', 'ﻳ', 'ﻴ', true, true), // ي
// Common Persian/Urdu additions
['پ'] = new ArabicForm('ﭖ', 'ﭗ', 'ﭘ', 'ﭙ', true, true), // پ
['چ'] = new ArabicForm('ﭺ', 'ﭻ', 'ﭼ', 'ﭽ', true, true), // چ
['ژ'] = new ArabicForm('ﮊ', 'ﮋ', '\0', '\0', true, false), // ژ
['ک'] = new ArabicForm('ﮎ', 'ﮏ', 'ﮐ', 'ﮑ', true, true), // ک
['گ'] = new ArabicForm('ﮒ', 'ﮓ', 'ﮔ', 'ﮕ', true, true), // گ
['ی'] = new ArabicForm('ﯨ', 'ﯩ', 'ﯪ', 'ﯫ', true, true) // ی
};
private object OnBetterChat(Dictionary<string, object> data)
{
object raw;
if (data == null || !data.TryGetValue("Message", out raw))
return null;
string message = raw as string;
if (string.IsNullOrEmpty(message) || !ContainsArabic(message))
return null;
data["Message"] = FixMessage(message);
return data;
}
private string FixMessage(string message)
{
string[] words = message.Split(
new[] { ' ', '\t', '\r', '\n' },
StringSplitOptions.RemoveEmptyEntries);
if (words.Length == 0)
return message;
List<string> output = new List<string>();
int index = 0;
while (index < words.Length)
{
if (ContainsArabic(words[index]))
{
List<string> rtlRun = new List<string>();
while (index < words.Length &&
(ContainsArabic(words[index]) || IsNumber(words[index])))
{
rtlRun.Add(words[index]);
index++;
}
for (int i = rtlRun.Count - 1; i >= 0; i--)
{
string token = rtlRun[i];
if (ContainsArabic(token))
output.Add(ReverseForRust(ShapeArabic(token)));
else
output.Add(token);
}
}
else
{
output.Add(words[index]);
index++;
}
}
return string.Join(" ", output.ToArray());
}
private string ShapeArabic(string text)
{
StringBuilder result = new StringBuilder(text.Length);
int i = 0;
while (i < text.Length)
{
char current = text[i];
// Lam-Alef ligatures: لا، لأ، لإ، لآ
if (current == 'ل' &&
i + 1 < text.Length &&
IsAlefVariant(text[i + 1]))
{
bool joinsPrevious = CanJoinWithPrevious(text, i);
result.Append(GetLamAlefLigature(text[i + 1], joinsPrevious));
i += 2;
continue;
}
ArabicForm currentForm;
if (!Forms.TryGetValue(current, out currentForm))
{
result.Append(current);
i++;
continue;
}
int previousIndex = FindPreviousBaseCharacter(text, i - 1);
int nextIndex = FindNextBaseCharacter(text, i + 1);
bool currentJoinsPrevious = false;
bool currentJoinsNext = false;
if (previousIndex >= 0)
{
ArabicForm previousForm;
if (Forms.TryGetValue(text[previousIndex], out previousForm))
{
currentJoinsPrevious =
previousForm.ConnectsToNext &&
currentForm.ConnectsToPrevious;
}
}
if (nextIndex >= 0)
{
ArabicForm nextForm;
if (Forms.TryGetValue(text[nextIndex], out nextForm))
{
currentJoinsNext =
currentForm.ConnectsToNext &&
nextForm.ConnectsToPrevious;
}
}
result.Append(ChooseForm(currentForm, currentJoinsPrevious, currentJoinsNext));
i++;
}
return result.ToString();
}
private bool CanJoinWithPrevious(string text, int currentIndex)
{
int previousIndex = FindPreviousBaseCharacter(text, currentIndex - 1);
if (previousIndex < 0)
return false;
ArabicForm previous;
ArabicForm current;
return Forms.TryGetValue(text[previousIndex], out previous) &&
Forms.TryGetValue(text[currentIndex], out current) &&
previous.ConnectsToNext &&
current.ConnectsToPrevious;
}
private static char ChooseForm(
ArabicForm form,
bool joinsPrevious,
bool joinsNext)
{
if (joinsPrevious && joinsNext && form.Medial != '\0')
return form.Medial;
if (joinsPrevious && form.Final != '\0')
return form.Final;
if (joinsNext && form.Initial != '\0')
return form.Initial;
return form.Isolated;
}
private static char GetLamAlefLigature(
char alef,
bool joinsPrevious)
{
switch (alef)
{
case 'آ': return joinsPrevious ? 'ﻶ' : 'ﻵ'; // لآ
case 'أ': return joinsPrevious ? 'ﻸ' : 'ﻷ'; // لأ
case 'إ': return joinsPrevious ? 'ﻺ' : 'ﻹ'; // لإ
default: return joinsPrevious ? 'ﻼ' : 'ﻻ'; // لا
}
}
private static bool IsAlefVariant(char character)
{
return character == 'ا' ||
character == 'آ' ||
character == 'أ' ||
character == 'إ';
}
private static int FindPreviousBaseCharacter(string text, int start)
{
for (int i = start; i >= 0; i--)
{
if (IsTransparentMark(text[i]))
continue;
return i;
}
return -1;
}
private static int FindNextBaseCharacter(string text, int start)
{
for (int i = start; i < text.Length; i++)
{
if (IsTransparentMark(text[i]))
continue;
return i;
}
return -1;
}
private static bool IsTransparentMark(char character)
{
return
(character >= 'ً' && character <= 'ٟ') ||
character == 'ٰ' ||
(character >= 'ۖ' && character <= 'ۭ');
}
private static bool ContainsArabic(string text)
{
if (string.IsNullOrEmpty(text))
return false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if ((c >= '' && c <= 'ۿ') ||
(c >= 'ݐ' && c <= 'ݿ') ||
(c >= 'ࢠ' && c <= 'ࣿ') ||
(c >= 'ﭐ' && c <= '﷿') ||
(c >= 'ﹰ' && c <= ''))
{
return true;
}
}
return false;
}
private static bool IsNumber(string text)
{
if (string.IsNullOrEmpty(text))
return false;
for (int i = 0; i < text.Length; i++)
{
if (!char.IsDigit(text[i]))
return false;
}
return true;
}
private static string ReverseForRust(string text)
{
// Reverse visual units rather than raw characters so Arabic
// vowel marks remain attached to their base letter.
List<string> units = new List<string>();
for (int i = 0; i < text.Length; i++)
{
StringBuilder unit = new StringBuilder();
unit.Append(text[i]);
while (i + 1 < text.Length && IsTransparentMark(text[i + 1]))
{
i++;
unit.Append(text[i]);
}
units.Add(unit.ToString());
}
StringBuilder result = new StringBuilder(text.Length);
for (int i = units.Count - 1; i >= 0; i--)
{
string unit = units[i];
if (unit.Length == 1)
result.Append(MirrorBracket(unit[0]));
else
result.Append(unit);
}
return result.ToString();
}
private static char MirrorBracket(char character)
{
switch (character)
{
case '(': return ')';
case ')': return '(';
case '[': return ']';
case ']': return '[';
case '{': return '}';
case '}': return '{';
case '<': return '>';
case '>': return '<';
default: return character;
}
}
}
}