اذهب إلى المحتوى

مطابقة عدة مجموعات نمطية في التعابير النمطية RegEx


ابراهيم الخضور

المجموعة الملتقطة capturing group هي الجزء الذي يضم محارف بين قوسين (...) في أي تعبيير نمطي RegEx، ولها تأثيران اثنان:

  1. تسمح بالحصول على جزء من التطابق مثل عنصر مستقل ضمن مصفوفة النتائج.
  2. يُطبق المحدد الكمي quantifier على مجموعة الالتقاط كلها إذا وضع بعد القوسين مباشرةً.

أمثلة عن مطابقة عدة مجموعات

لنتعرف كيفية عمل الأقواس من خلال الأمثلة.

مثال gogogo

يفيد النمط +go دون أقواس في إيجاد المحرف g يليه المحرف o مكررًا مرةً أو أكثر، مثل goooo أو goooooo، وإذا وضعنا محارف النمط السابق بين قوسين +(go)، فسيعني ذلك go أو gogo أو gogogo وهكذا.

alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"

مثال نطاق موقع ويب

نحتاج إلى تعبير نمطي للبحث عن نطاق موقع ويب، مثل النطاقات التالية:

mail.com
users.mail.com
smith.users.mail.com

يتألف النطاق من كلمات متتالية تفصل بينها نقاط، ويقابل ذلك التعبير النمطي +w+\.)+\w\):

let regexp = /(\w+\.)+\w+/g;

alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com

سيعمل النمط السابق، لكنه سيواجه مشكلةً عندما تحتوي الكلمات على شرطة قصيرة -، مثل my-site.com، فلا يعتبر هذا المحرف من محارف الصنف w\، وسنحل هذه المشكلة باستبدال التعبير w\ بالمجموعة [-w\]، في كل كلمة عدا الأخيرة، وسيصبح النمط بالشكل +w-]+\.)+\w\]).

مثال البريد الإلكتروني

يمكن توسيع المثال السابق لإنشاء تعبير نمطي لبريد إلكتروني اعتمادًا على النمط السابق، وما دام للبريد الإلكتروني الشكل name@domain، فيمكن أن تكون أي كلمة هي الاسم، حيث يُسمح ضمنها بالشرطة القصيرة أو النقاط، وبلغة التعبير النمطي ستكون [.-w\]، وسيكون التعبير النمطي للبريد الإلكتروني /+[-w.-]+@([\w-]+\.)+[\w\]/

let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;

alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk

ليس هذا التعبير مثاليًا، لكنه سيعمل في معظم الأحيان، وسيساعدك في التخلص من الأخطاء الكتابية، وتأكد أن الطريقة الحقيقية الوحيدة للتحقق من بريد هو استلامه للرسالة التي أرسلتها!

المحتوى الموجود بين قوسين عند البحث عن تطابق

تُرقّم الأقواس من اليسار إلى اليمين، وسيتذكر المحرك المحتوى الذي يتطابق مع كل قوس، ويسمح بالحصول على هذه التطابقات ضمن النتيجة، ويبحث التابع (str.match(regexp عن التطابق الأول، ويعيد النتيجة في مصفوفة عندما لا تُستخدم الراية g ضمن التعبير regexp، حيث ستجد ضمن المصفوفة:

  1. التطابق بشكله الكامل في الموقع 0.
  2. محتوى القوس الأول في الموقع 1.
  3. محتوى القوس الثاني في الموقع 2.
  4. وهكذا…

regexp-nested-groups.png

فلو أردنا مثلًا البحث عن وسوم HTML من النمط <?*.>، ثم معالجة النتائج، فمن المناسب أن نحصل على محتوى كل وسم ضمن متغير خاص به، وعندما نغلّف المحتوى الداخلي للوسم ضمن قوسين، بالشكل التالي <(?*.)>، فسنحصل على الوسم كاملًا، وليكن <h1>، وعلى محتوى هذا الوسم (أي النص h1) ضمن النتيجة:

regexp-nested-groups.png

let str = '<h1>Hello, world!</h1>';

let tag = str.match(/<(.*?)>/);

alert( tag[0] ); // <h1>
alert( tag[1] ); // h1

المجموعات المتداخلة

يمكن أن تتداخل الأقواس، وعندها ستُرقَّم أيضًا من اليسار إلى اليمين، فعندما نبحث عن وسم ضمن الوسم <span class="my"> مثلًا، فلربما نريد الحصول على:

  1. محتوى الوسم كاملًا "span class="my.
  2. اسم الوسم: span.
  3. سمات الوسم: "class="my.

لنضف الأقواس إلى النمط <(([a-z]+)\s*([^>]*))>، لاحظ كيف تُرقَّم الأقواس من اليسار إلى اليمين:

regexp-nested-groups.png

let str = '<span class="my">';

let regexp = /<(([a-z]+)\s*([^>]*))>/;

let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"

سنجد دائمًا التطابق الكامل في الموقع صفر من المصفوفة، ثم المجموعات مرقمةً من اليسار إلى اليمين بواسطة القوس المفتوح، حيث تُعاد المجموعة الأولى في الموقع الأول [result[1، تليها الثانية الناتجة عن القوس المفتوح الثاني (+[a-z]) ضمن [result[2، ثم نتيجة التطابق مع النمط (‎[^>]*‎) ضمن [result[3، وستكون نتيجة كل مجموعة بصيغة نص.

المجموعات الاختيارية

حتى لو كانت المجموعة اختياريةً وغير موجودة ضمن التطابق، كأن يكون لها المحدد الكمي ?(...)، فسيبقى مكانها محجوزًا ضمن المصفوفة، وقيمته هي undefined، فلو تأملنا مثلًا التعبير ?(a(z)?(c، فسنجد أنه يبحث عن "a" متبوعًا -اختياريًا- بالحرف "z"، ومتبوعًا -اختياريًا أيضًا- بالحرف "c"، لأن المحدد الكمي ? يعني محرفًا أو لا شيء، فلو طبّقنا التعبير السابق على نص مكون من الحرف a فقط، فستكون النتيجة:

let match = 'a'.match(/a(z)?(c)?/);

alert( match.length ); // 3
alert( match[0] ); // a (whole match)
alert( match[1] ); // undefined
alert( match[2] ); // undefined

سيكون طول المصفوفة 3 علمًا أن كل المجموعات فارغة!

لكن لو كان النص هو ac:

let match = 'ac'.match(/a(z)?(c)?/)

alert( match.length ); // 3
alert( match[0] ); // ac (whole match)
alert( match[1] ); // undefined, because there's nothing for (z)?
alert( match[2] ); // c

سيبقى طول المصفوفة 3، لكنك لن تجد تطابقًا يقابل المجموعة (z)?، وستكون النتيجة ["ac", undefined, "c"].

البحث عن كل التطابقات ضمن المجموعات: التابع matchAll

اقتباس

**التابع matchAll هو تابع جديد، وقد نحتاج إلى موائمة برمجية polyfill لتعويض نقص دعمه في بعض المتصفحات القديمة التي لا تدعمه، لكن يمكنك استخدام بعض الموائمات الخاصة.

لن يعيد التابع match محتوى المجموعات إذا استخدم للبحث بوجود الراية g، والتي تعني إيجاد كل التطابقات، وسنحاول في المثال التالي إيجاد كل الوسوم في النص:

let str = '<h1> <h2>';

let tags = str.match(/<(.*?)>/g);

alert( tags ); // <h1>,<h2>

لاحظ أن النتيجة هي مصفوفة تحتوي على التطابقات كاملةً لكن دون تفاصيل، أي دون محتوى كل تطابق، لكننا نحتاج عمليًا إلى ذلك المحتوى، وسيساعدنا البحث باستخدام التابع (str.matchAll(regexp على استخلاص ذلك المحتوى، فقد أضيف هذا التابع إلى JavaScript بعد فترة طويلة من إضافة match مثل نسخة جديدة ومحسنة منه.

يشابه matchAll التابع match، مع وجود ثلاثة اختلافات، وهي:

  1. لا يعيد مصفوفةً، بل كائنًا قابلًا للتكرار iterable object.
  2. يعيد كل تطابق مثل مصفوفة تحتوي مجموعات عند استخدام الراية g.
  3. عندما لا يجد تطابقات فلن يعيد null، بل كائنًا فارغًا قابلًا للتكرار.

إليك مثالًا:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

// النتائج ليست مصفوفة بل كائن قابل للتعداد
alert(results); // [object RegExp String Iterator]

alert(results[0]); // undefined (*)

results = Array.from(results); // تحويل النتيجة إلى مصفوفة عادية

alert(results[0]); // <h1>,h1 (1st tag)
alert(results[1]); // <h2>,h2 (2nd tag)

إنّ الاختلاف الأول مهم جدًا كما يوضّحه السطر "(*)"، فلا يمكن الحصول على التطابق في الموقع [results[0، لأن الكائن لا يمثل مصفوفةً زائفةً pseudoarray، ويمكننا تحويلها إلى مصفوفة حقيقية باستخدام Array.from، وستجد العديد من التفاصيل عن المصفوفات الزائفة والكائنات القابلة للتكرار في المقال Iterables.

لا حاجة لتحويل المصفوفة باستخدام Array.from إذا كنا سنشكل حلقةً من النتائج:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

for(let result of results) {
  alert(result);
  // first alert: <h1>,h1
  // second: <h2>,h2
}

أو عند استخدام التفكيك destructuring:

let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

يشابه تنسيق كل تطابق يعيده التابع matchAll التنسيق الذي يعيده match دون الراية g، وهذا التنسيق هو مصفوفة مع خصائص إضافية index التي تطابق الفهرس في النص، وinput الذي يعني النص الأصلي:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

let [tag1, tag2] = results;

alert( tag1[0] ); // <h1>
alert( tag1[1] ); // h1
alert( tag1.index ); // 0
alert( tag1.input ); // <h1> <h2>

انتبه، لماذا ستكون نتيجة التابع كائنًا قابلًا للتكرار وليس مصفوفةً؟

السبب بسيط وهو التحسين، فلن يُنفِّذ الاستدعاء عملية البحث، بل سيعيد كائنًا قابلًا للتكرار لا يحتوي على النتيجة مبدئيًا، ويُنفَّذ البحث في كل مرة نكرره (ضمن حلقة مثلًا)، وبالتالي سيجد العدد المطلوب من النتائج تمامًا، فإذا كان من المحتمل مثلًا وجود 100 تطابق، لكننا وجدنا في حلقة for..of خمسةً فقط، وقررنا أن هذا كاف وأوقفنا الحلقة، فلن يستهلك المحرك وقتًا إضافيًا في إيجاد التطابقات 95 الباقية.

المجموعات المسماة

يصعب تذكر المجموعات بأرقامها، على الرغم من بساطته في الأنماط البسيطة، لكن عند البحث عن أنماط أكثر تعقيدًا فلن يكون ترقيم الأقواس أمرًا مناسبًا، وسنجد أن خيار تسمية الأقواس هو الأفضل، ونسمي الأقواس بوضع الاسم بالشكل التالي <name>? مباشرةً بعد القوس، ولنبحث مثلًا عن تاريخ وفق التنسيق "يوم-شهر-سنة":

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";

let groups = str.match(dateRegexp).groups;

alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30

حيث سنجد المجموعات عبر الخاصية groups. لكل تطابق، ويمكن إيجاد جميع التواريخ باستخدام الراية g، كما ينبغي استخدام التابع matchAll للحصول على التطابق كاملًا بالإضافة إلى المجموعات:

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;

let str = "2019-10-30 2020-01-01";

let results = str.matchAll(dateRegexp);

for(let result of results) {
  let {year, month, day} = result.groups;

  alert(`${day}.${month}.${year}`);
  // first alert: 30.10.2019
  // second: 01.01.2020
}

مطابقة مجموعات ثم تنفيذ عملية استبدال

يسمح التابع (str.replace(regexp, replacement الذي يستبدل محتوى الأقواس ضمن النص replacement، بكل التطابقات regexp التي يجدها في النص str، وينفذ ذلك باستخدام الرمز n$، حيث n هو رقم المجموعة، وإليك مثالًا:

let str = "John Bull";
let regexp = /(\w+) (\w+)/;

alert( str.replace(regexp, '$2, $1') ); // Bull, John

ستتغير العملية باستخدام <name>$ في الأقواس المسماة، ولتغيير تنسيق التاريخ مثلًا من "يوم -شهر-سنة" إلى "سنة.شهر.يوم":

let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;

let str = "2019-10-30, 2020-01-01";

alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020

استثناء التقاط المجموعات باستخدام :?

نحتاج أحيانًا إلى الأقواس لتطبيق المحددات الكمية بطريقة صحيحة، لكننا لا نحتاج إلى محتواها ضمن النتائج، لذلك يمكن استثناء المجموعات باستخدام :? في البداية، فإذا أردنا مثلًا إيجاد النمط +(go)، لكننا لا نحتاج إلى محتوى الأقواس (go) في عنصر مصفوفة مستقل، فيمكننا صياغة النمط بالشكل `+(go?:)، وإليك مثالًا:

let str = "Gogogo John!";

// ?: exludes 'go' from capturing
let regexp = /(?:go)+ (\w+)/i;

let result = str.match(regexp);

alert( result[0] ); // Gogogo John (تطابق كامل)
alert( result[1] ); // John
alert( result.length ); // 2 (لا مزيد من العناصر ضمن المصفوفة)

الخلاصة

  1. تُجمِّع الأقواس أجزاءً من التعبير النمطي ليُطبَّق المحدد الكمي عليها مثل زمرة واحدة.
  2. تُرقَّم أقواس المجموعات من اليسار إلى اليمين، كما يمُكن أن تُسمّى اختياريًا باستخدام النمط (...<name>?).
  3. يمكن الحصول على المحتوى الموجود داخل الأقواس -الذي يحقق التطابق- بصورة مستقلة ضمن النتيجة، حيث:
  • يعيد التابع str.match المجموعة الملتقطة عند استخدام الراية فقط.
  • يعيد التابع str.matchAll المجموعات الملتقطة دومًا.
  1. إذا لم تُسمَّ الأقواس فسنحصل على محتوياتها ضمن المصفوفة وفقًا لتسلسل ترقيمها، كما يمكن الحصول على محتويات الأقواس المسماة من خلال الخاصية groups..
  2. يمكن استخدام محتويات الأقواس في النص البديل للتابع، إما عبر أرقامها من خلال n$، أو أسمائها من خلال <name>$.
  3. يمكن استثناء مجموعة من الترقيم باستخدام النمط :? قبلها، وذلك عندما نريد تطبيق مُحصٍ quantifier على كامل المجموعة، لكننا لا نريد أن تظهر محتويات المجموعة -الموجودة بين قوسين- في عنصر مستقل ضمن مصفوفة النتيجة، كما لا يمكن الإشارة إلى هذه الأقواس عند استخدام تابع الاستبدال.

مهام لإنجازها

تحقق من عنوان MAC

يتكون عنوان MAC لواجهة اتصال مع الشبكات من 6 أرقام ست عشرية ذات خانتين تفصل بينها نقطتان ":"، مثل العنوان التالي '01:32:54:67:89:AB'، اكتب تعبيرًا نظاميًا يتحقق من أن النص هو عنوان MAC.

let regexp = /your regexp/;

alert( regexp.test('01:32:54:67:89:AB') ); // true

alert( regexp.test('0132546789AB') ); // false (no colons)

alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, must be 6)

alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ at the end)

الحل: يُعطى التعبير النمطي الذي يبحث عن عدد ست عشري من خانتين بالشكل: {2}[‎0-9a-f] مفترضين استخدام الراية i. سنحتاج الآن إلى هذا النمط وخمسة أنماط أخرى مشابهة له، وبالتالي سيكون التعبير النمطي على الشكل:

[0-9a-f]{2}(:[0-9a-f]{2}){5}

ولكي نجبر التعبير على التقاط كامل النص الموافق، لابد من وضع محرفي ارتكاز البداية والنهاية $...^ إليك شيفرة الحل بشكلها الكامل:

let regexp = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i;

alert( regexp.test('01:32:54:67:89:AB') ); // true

alert( regexp.test('0132546789AB') ); // false (no colons)

alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, need 6)

alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ in the end)

أوجد الألوان التي تنسّق بالشكل abc# أو abcdef

اكتب تعبيرًا نمطيًا يبحث عن الألوان المكتوبة وفق أحد التنسيقين abc# أو abcdef#، أي المحرف # يليه ثلاث أو ست أرقام ست عشرية.

let regexp = /your regexp/g;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef

لاحظ أنه لا ينبغي الحصول على تطابقات تحوي أربع أو خمس أرقام ست عشرية، بل 3 أو 6 فقط.

الحل:

سيكون التعبير النمطي المناسب للبحث عن شيفرة لون ثلاثية الأرقام كالتالي:

/#[a-f0-9]{3}/i

يمكننا أيضًا إضافة ثلاث أرقام ست عشرية أخرى بالضبط فلن نحتاج أكثر أو أقل، لكون الشيفرة اللونية مزيج من ثلاث أو ستة أرقام.

لنستخدم إذًا المكمم {1,2} بعد وضع الصيغة السابقة للتعبير النمطي بين قوسين:

/#([a-f0-9]{3}){1,2}/i

لاحظ الشيفرة:

let regexp = /#([a-f0-9]{3}){1,2}/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef #abc

تواجهنا مشكلة صغيرة هنا، فسيجد التعبير النمطي المحارف abc# ضمن abcd#. يمكن وضع النمط b\ في النهاية لحل المشكلة:

let regexp = /#([a-f0-9]{3}){1,2}\b/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(regexp) ); // #3f3 #AA00ef

أوجد كل الأرقام

اكتب تعبيرًا نمطيًا يبحث عن كل الأعداد العشرية، بما فيها الصحيحة والعشرية ذات الفاصلة العائمة أو السالبة.

let regexp = /your regexp/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(regexp) ); // -1.5, 0, 2, -123.4

الحل:

يُعطى نمط العدد العشري الموجب بوجود القسم العشري منه على الشكل:

\d+(\.\d+)?

لنضع الإشارة السالبة - لتكون اختيارية في بداية النمط:

let regexp = /-?\d+(\.\d+)?/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(regexp) );   // -1.5, 0, 2, -123.4

فسر العبارات الرياضية

تتكون العملية الحسابية من عددين بينهما إشارة عمليات، مثل:

  • 1 + 2
  • 1.2 * 3.4
  • -3 / -6
  • -2 - 2

قد تكون العملية "+"أو "-" أو "*" أو "/"، وقد توجد مساحات فارغة قبل أو بعد أو بين الأجزاء.

أنشئ تابعًا (parse(expr يقبل العبارة معاملًا، ويعيد مصفوفةً من ثلاثة عناصر، وهي:

  1. العدد الأول.
  2. العامل الرياضي (إشارة العملية).
  3. العدد الثاني.
let [a, op, b] = parse("1.2 * 3.4");

alert(a); // 1.2
alert(op); // *
alert(b); // 3.4

الحل: يُعطى التعبير النمطي لإيجاد عدد كما رأينا في المهمة السابقة كالتالي:

-?\d+(\.\d+)?

تُعطى العملية الرياضية وفق النمط التالي [/*+-] ولابد من وضع المحرف - في بداية الأقواس المربعة لأنها ستعنى مجالًا من المحارف إن وضعت في المنتصف ونحن نريد فقط المحرف - بحد ذاته. لايد أيضًا من تجاوز المحرف /في التعبير النمطي كالتالي /.../ وهذا ما سنفعله لاحقًا.

نحتاج إلى عدد ثم عملية ثم عدد آخر وقد تكون هناك مساحات فارغة اختيارية بينهم وبالتالي سيكون التعبير النمطي كاملًا:

-?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?

يتكون التعبير النمطي من ثلاثة أقسام يفصل بينها ‎s*‎\:

/-?\d+(\.\d+)?/   // العدد الأول
/[-+*/],/   // رمز العملية
/-?\d+(\.\d+)?/   // العدد الثاني

يمثل القسم الأول العدد الأول والقسم الثاني العملية الحسابية والثالث العدد الثاني، ولكي يظهر كل قسم كنتيجة مستقلة ضمن مصفوفة النتيجة، سنضع كل قسم ضمن قوسين:

/(-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?)/

إليك الشيفرة:

let regexp = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;
alert( "1.2 + 12".match(regexp) );

تتضمن النتائج:

  1. التطابق كاملًا : "result[0] == "1.2 + 12
  2. المجموعة الأولى (‎-?\d+(\.\d+)?‎) وتمثل العدد الأول مع أجزائه العشرية: "1.2"
  3. المجموعة الثانية ‎(\.\d+)?‎ وتمثل الجزء العشري الأول: "‎.2"
  4. المجموعة الثالثة ‎([-+*\/])‎ وتمثل العملية الحسابية: "+"
  5. المجموعة الرابعة ‎(-?\d+(\.\d+)?)‎ وتمثل العدد الثاني: "12"
  6. المجموعة الخامسة ‎(\.\d+)?‎ والتي تمثل الجزء العشري من العدد الثاني وهو في الواقع غير موجود undefined

نحتاج في الواقع إلى العددين والعملية الحسابية دون الأجزاء العشرية لذلك سنجعل التعبير أكثر وضوحًا ليلائم ما نريده. سنزيل العنصر الأول من المصفوفة والذي يمثل التطابق الكامل بإجراء انزياح لمصفوفة النتيجة array.shift.

يمكن التخلص من الأجزاء العشرية (‎.\d+‎) في المجموعة الثانية والرابعة (أي النقط 3 و 4) من المصفوفة بوضع المحرف ? في بداية كل مجموعة.

إليك الحل النهائي:

function parse(expr) {
  let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;

  let result = expr.match(regexp);

  if (!result) return [];
  result.shift();

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45

ترجمة -وبتصرف- للفصل Capturing Groups من سلسلة The Modern JavaScript Tutorial.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...