ابزارهای متن

یکسان‌سازی متن فارسی و عربی، اصلاح نیم‌فاصله، تشخیص خط و جهت متن و ساخت اسلاگ برای نشانی اینترنتی، با رفتار دقیق هر ابزار.

در این صفحه
  1. Normalizer
    1. normalize()
    2. fixHalfSpace()
    3. clean()
  2. Detector
    1. isPersian() و isArabic()
    2. جهت و RTL
    3. isRtlLocale()
  3. Slugify
    1. جداکننده

Normalizer#

RtlyKit\Text\Normalizer متن فارسی را برای مقایسه و ذخیره آماده می‌کند. ورودی کاربر معمولاً شکل‌های عربی و فارسی حرف‌ها را قاطی می‌کند (صفحه‌کلید عربی «ي» و «ك» تایپ می‌کند). گاهی اعراب و کشیده و فاصلهٔ اضافه هم دارد. آن‌وقت یک واژه روی صفحه یکسان دیده می‌شود، ولی در مقایسهٔ رشته‌ای یا جست‌وجوی پایگاه داده با هم نمی‌خوانند.

normalize()#

Normalizer::normalize($text, $removeDiacritics = true) این کارها را می‌کند:

  • حرف‌های عربی را فارسی می‌کند: «ك» به «ک»، «ي» و «ى» و «ے» به «ی»، «ة» و «ۀ» به «ه»، «ؤ» به «و»، «إ» و «أ» و «ٱ» به «ا».
  • رقم‌های عربی-هندی (٠-٩) را فارسی (۰-۹) می‌کند. رقم‌های انگلیسی دست نمی‌خورند.
  • کشیده (تطویل، U+0640) را حذف می‌کند.
  • اعراب (U+064B تا U+065F و U+0670) را حذف می‌کند، مگر اینکه آرگومان دوم را false بدهید.
  • هر دنبالهٔ فاصله (فاصله، تب، خط جدید) را به یک فاصله تبدیل می‌کند و دو سر متن را تمیز می‌کند.
<?php
require 'vendor/autoload.php';

use RtlyKit\Text\Normalizer;
use function RtlyKit\normalize_text;

echo Normalizer::normalize("علي  كتاب  ١٢٣ 456"), "\n";    // علی کتاب ۱۲۳ 456
echo Normalizer::normalize("كــتاب"), "\n";                 // کتاب
echo Normalizer::normalize("  سلام \n\t دنیا  "), "\n";     // سلام دنیا
echo Normalizer::normalize("أحمد إبراهيم مؤمن ة"), "\n";    // احمد ابراهیم مومن ه
echo Normalizer::normalize("ثَبْت  ١٢٣ 456"), "\n";         // ثبت ۱۲۳ 456
echo Normalizer::normalize("ثَبْت", false), "\n";           // ثَبْت (اعراب می‌ماند)
echo normalize_text('كتاب'), "\n";                          // کتاب
به چه دست نمی‌زند. همزهٔ تنهای «ء» و «ئ» عمداً می‌ماند، چون در واژه‌های فارسی و عربی معنا دارد. نیم‌فاصله (ZWNJ) هم می‌ماند: «می‌خواهم» بایت‌به‌بایت همان است. حرف‌های انگلیسی هم تغییر نمی‌کنند.

تابع کمکی normalize_text() همان Normalizer::normalize() با مقدارهای پیش‌فرض است.

fixHalfSpace()#

نیم‌فاصله (ZWNJ، U+200C) چیزی است که «می‌خواهم» را درست نشان می‌دهد. کپی و پیست اغلب چند نیم‌فاصلهٔ پشت‌هم، یا نیم‌فاصله کنار فاصله، یا نیم‌فاصله آخر متن به جا می‌گذارد. fixHalfSpace() این‌ها را مرتب می‌کند:

  • خط تیرهٔ نرم (U+00AD) را حذف می‌کند.
  • چند نیم‌فاصلهٔ پشت‌هم را یکی می‌کند.
  • نیم‌فاصله‌ای را که به فاصله چسبیده (قبل یا بعد) حذف می‌کند.
  • نیم‌فاصلهٔ اول و آخر متن را حذف می‌کند.
$a = "می\u{200C}\u{200C}\u{200C}خواهم";
echo preg_match_all('/./u', $a), ' ', preg_match_all('/./u', Normalizer::fixHalfSpace($a)), "\n";   // 10 8

$b = "\u{200C}سلام \u{200C}دنیا\u{200C}";
echo preg_match_all('/./u', $b), ' ', preg_match_all('/./u', Normalizer::fixHalfSpace($b)), "\n";   // 12 9
echo Normalizer::fixHalfSpace($b), "\n";                                  // سلام دنیا

نیم‌فاصلهٔ جاافتاده را اضافه نمی‌کند و فاصله‌های معمولی را هم یکی نمی‌کند.

clean()#

Normalizer::clean() نسخهٔ کامل برای ذخیره و جست‌وجوست. اول normalize() را اجرا می‌کند، بعد fixHalfSpace() را، و بعد نویسه‌های نامرئی را حذف می‌کند: فاصلهٔ بدون پهنا (U+200B)، اتصال‌دهندهٔ بدون پهنا (U+200D) و نشانهٔ ترتیب بایت (U+FEFF). ZWNJ تنها نویسهٔ بدون پهنایی است که می‌ماند.

$c = "كتاب\u{200B}ي  ١٢٣";
echo Normalizer::clean($c), "\n";   // کتابی ۱۲۳

اگر فقط normalize() را روی همین متن بزنید، فاصلهٔ نامرئی وسط واژه می‌ماند. برای همین clean() هست. متنی را که کاربر وارد کرده، پیش از ذخیره یا نمایه‌سازی، با آن تمیز کنید.

Detector#

RtlyKit\Text\Detector دربارهٔ خط، زبان و جهت متن جواب می‌دهد. همهٔ متدها استاتیک‌اند و خطا پرتاب نمی‌کنند.

isPersian() و isArabic()#

هر دو روش تقریبی‌اند و برای متنی با خط عربی کار می‌کنند. فارسی از حرف‌هایی شناخته می‌شود که فقط در فارسی هستند (پ چ ژ گ، شکل فارسی ک و ی و رقم‌های فارسی). عربی از ي ك ى ة و رقم‌های عربی-هندی شناخته می‌شود. حرف‌های مشترک، مثل «ه» و شکل‌های همزه، حساب نمی‌شوند. اگر حرف‌های ویژهٔ فارسی دست‌کم به اندازهٔ حرف‌های ویژهٔ عربی باشند، متن فارسی است. پس در حالت مساوی یا وقتی هیچ حرف تمایزی نباشد هم فارسی حساب می‌شود.

use RtlyKit\Text\Detector;

var_dump(Detector::isPersian('سلام'));        // bool(true)، حرف تمایزی ندارد، پیش‌فرض فارسی است
var_dump(Detector::isPersian('گچپژ'));        // bool(true)
var_dump(Detector::isArabic('مرحبا بكم'));    // bool(true)
var_dump(Detector::isArabic('كتاب ي'));       // bool(true)
var_dump(Detector::isPersian('hello'));       // bool(false)، اصلاً خط عربی ندارد
var_dump(Detector::isPersian('۱۲۳'));         // bool(true)، رقم‌های فارسی
var_dump(Detector::isArabic('٣٤٥'));          // bool(true)، رقم‌های عربی-هندی
var_dump(Detector::isPersian(''));            // bool(false)
خوب است بدانید. این روش بر شمارش حرف‌ها بنا شده و تشخیص‌دهندهٔ کامل زبان نیست. متن خیلی کوتاه، نام‌ها و واژه‌های عربی که فارسی نوشته شده‌اند مبهم‌اند و متنی که هر دو زبان را دارد به هر سو می‌رود. برای انتخاب یک پیش‌فرض معقول خوب است. برای تصمیمی که باید حتماً درست باشد، تنها به آن تکیه نکنید.

جهت و RTL#

  • containsRtl($text): آیا متن نویسه‌ای از یک خط راست‌به‌چپ دارد (عبری، عربی و ضمیمه‌هایش، سریانی، تانا، نکو و غیره، شکل‌های نمایشی عربی) یا نشانهٔ RLM.
  • direction($text): 'rtl' یا 'ltr' بر اساس اولین حرف قوی، طبق قاعدهٔ دوسویهٔ یونیکد. رقم، نشانه‌گذاری و فاصله خنثی‌اند. متنی که هیچ حرفی ندارد 'ltr' می‌دهد.
  • isHebrew($text): آیا متن حرف عبری دارد.
var_dump(Detector::containsRtl('abc'));           // bool(false)
echo Detector::direction('سلام hello'), "\n";    // rtl
echo Detector::direction('Hello مرحبا'), "\n";   // ltr (اولین حرف لاتین است)
echo Detector::direction('۱۲۳'), "\n";           // ltr (رقم خنثی است و حرف نیست)
var_dump(Detector::isHebrew('שלום'));            // bool(true)

توابع کمکی contains_rtl() و text_direction() همان containsRtl() و direction() هستند. برای صفت dir در HTML، روی متنی که کاربر نوشته از direction() استفاده کنید.

isRtlLocale()#

isRtlLocale($locale) می‌گوید یک برچسب زبان راست‌به‌چپ نوشته می‌شود یا نه. برچسب را با -، _، . و @ می‌شکند، به بزرگی و کوچکی حرف و فاصله‌های دو سر توجه نمی‌کند، و اگر زبان راست‌به‌چپ باشد (fa، ar، he، ur، ps، ug، dv، ckb، azb و ...) یا زیربرچسب خطش Arab یا Hebr باشد، true می‌دهد.

var_dump(Detector::isRtlLocale('fa_IR'));     // bool(true)
var_dump(Detector::isRtlLocale('ar-SA'));     // bool(true)
var_dump(Detector::isRtlLocale('ckb-IQ'));    // bool(true)
var_dump(Detector::isRtlLocale('az-Arab'));   // bool(true)، زیربرچسب خط تعیین‌کننده است
var_dump(Detector::isRtlLocale('ku'));        // bool(false)، «ku» ساده در فهرست زبان‌های RTL نیست
var_dump(Detector::isRtlLocale('en'));        // bool(false)
var_dump(Detector::isRtlLocale(''));          // bool(false)

Slugify#

Slugify::make($text, $separator = '-') عنوان را به اسلاگ مناسب نشانی اینترنتی تبدیل می‌کند و حرف‌های فارسی و عربی را همان‌طور خوانا نگه می‌دارد. به لاتین تبدیل نمی‌کند.

  1. متن را با Normalizer::normalize() یکسان می‌کند.
  2. فاصله و نیم‌فاصله را به جداکننده تبدیل می‌کند.
  3. هر چیزی را که حرف (از هر خط)، نشانهٔ ترکیبی، رقم (از هر دسته)، خط تیره یا زیرخط نیست حذف می‌کند. نشانه‌گذاری و اموجی می‌رود.
  4. خط تیره، زیرخط و جداکنندهٔ تکراری را یکی می‌کند و جداکننده را از دو سر برمی‌دارد.
  5. نتیجه را با UTF-8 به حرف کوچک تبدیل می‌کند.
use RtlyKit\Text\Slugify;

echo Slugify::make('سلام دنیا'), "\n";               // سلام-دنیا
echo Slugify::make("کتاب\u{200C}خانه ملی"), "\n";    // کتاب-خانه-ملی
echo Slugify::make('Hello, World!'), "\n";           // hello-world
echo Slugify::make('  A--B__C  '), "\n";             // a-b_c
echo Slugify::make('سلام!!! دنیا؟'), "\n";           // سلام-دنیا
echo Slugify::make('۱۲۳ ابر'), "\n";                 // ۱۲۳-ابر
echo Slugify::make('كتاب ي'), "\n";                  // کتاب-ی
echo Slugify::make('سلام 😀 دنیا'), "\n";            // سلام-دنیا
echo Slugify::make('عکس.jpg'), "\n";                 // عکسjpg
var_dump(Slugify::make('$%^&'));                    // string(0) ""

دو نکته. رقم‌ها همان دسته‌ای می‌مانند که تایپ شده‌اند (۱۲۳ فارسی می‌ماند). اگر رقم انگلیسی در نشانی می‌خواهید، اول Digits::toEnglish() را اجرا کنید. و نقطه حذف می‌شود، به جداکننده تبدیل نمی‌شود. پس پسوند فایل به نام می‌چسبد (عکسjpg). نام و پسوند را جدا اسلاگ کنید.

جداکننده#

هر رشته‌ای را می‌توانید به‌عنوان آرگومان دوم بدهید: '_'، رشتهٔ بلندتر، یا رشتهٔ خالی که واژه‌ها را بدون جداکننده می‌چسباند.

echo Slugify::make('سلام دنیا', '_'), "\n";        // سلام_دنیا
echo Slugify::make('سلام دنیا', ''), "\n";         // سلامدنیا
echo Slugify::make('Hello World', '--'), "\n";     // hello--world

جداکننده باید حداکثر ۶۴ بایت و UTF-8 معتبر باشد. جداکنندهٔ بلندتر RtlyKitException با کد input_too_long می‌دهد و جداکنندهٔ نامعتبر همان خطا را با invalid_argument:

try {
    Slugify::make('Hello World', str_repeat('x', 65));
} catch (\RtlyKit\Exceptions\RtlyKitException $e) {
    echo $e->getMessage(), ' [', $e->getErrorCode()->value, "]\n";
    // The slug separator is too long. [input_too_long]
}
اسلاگ خالی و یکتایی. عنوانی که فقط نشانه‌گذاری دارد، رشتهٔ خالی می‌دهد. عنوان‌های متفاوت هم می‌توانند یک اسلاگ بسازند. حالت خالی را در کدتان در نظر بگیرید (مثلاً به شناسه برگردید) و یکتایی را در ذخیره‌سازی خودتان تضمین کنید.
کدگذاری. Slugify هم برای متن و هم برای جداکننده UTF-8 معتبر می‌خواهد. بایت‌های نامعتبر در متن، RtlyKitException با کد invalid_argument می‌دهد. اسلاگ یک شناسه است، برای همین ورودی خراب را قبول نمی‌کند. Normalizer عمداً ملایم‌تر است: تا جایی که بتواند کار می‌کند و آنچه را نتواند پردازش کند همان‌طور برمی‌گرداند. حرف کوچک‌کردن با نگاشت ساده‌ٔ یونیکد انجام می‌شود و افزونهٔ PHP لازم ندارد.