/**
* parser.js
* JavaScript parser that reads commands.json and converts Arabic natural sentences
* into a list of actionable commands (actions).
*
* - Compatible with Node (v16+ with experimental JSON import) or bundlers.
* - Optional integration with Araby.js: uncomment import & normalization lines if available.
*
* Usage:
* node parser.js
*
* Output: printed JSON actions for example sentences.
*/
// إذا عندك Araby.js تقدر فك تعليق السطر التالي واستعمل normalization
// import { normalize, stripTashkeel } from "arabyjs";
// تحميل ملف JSON (في Node >= 18 يمكنك استخدام assert import)، أو استبدله بـ fetch في المتصفح.
import config from './commands.json' assert { type: "json" };
/* ---------- أدوات مساعدة للبحث عن مرادف في config ---------- */
function includesPhraseAnywhere(input, phrase) {
// مقارنة بسيطة: نبحث عن العبارة كما هي داخل النص
// نستخدم indexOf لضمان عمل العبارات متعددة الكلمات مثل "تحتوي على"
return input.indexOf(phrase) !== -1;
}
function findKeyByPhrase(input, category) {
// يبحث أول تطابق مرادف في category ويعيد المفتاح (key)
const dict = config[category];
if (!dict) return null;
for (const key of Object.keys(dict)) {
const synonyms = dict[key];
for (const ph of synonyms) {
if (includesPhraseAnywhere(input, ph)) {
return key;
}
}
}
return null;
}
function findAllKeysByPhrase(input, category) {
const dict = config[category];
if (!dict) return [];
const found = [];
for (const key of Object.keys(dict)) {
const synonyms = dict[key];
for (const ph of synonyms) {
if (includesPhraseAnywhere(input, ph)) {
found.push(key);
break;
}
}
}
return found;
}
/* ---------- تطبيع مبسط (اختياري استخدام Araby.js) ---------- */
function normalizeInputRaw(text) {
if (!text) return text;
let s = text.trim();
// اختيارات بسيطة: حذف الـ "ال" التعريفية المتجاورة لأجل التطابق
// ملاحظة: هذا تبسيط — لو استعملت Araby.js استخدم normalize(stripTashkeel(text))
s = s.replace(/\s+/g, ' ');
s = s.replace(/ال(?=[\u0600-\u06FF])/g, ''); // يزيل حرف "ال" قبل الكلمات العربية (مبدئي)
return s.toLowerCase();
}
/* ---------- تحليل جملة مفردة (جزء واحد بين "ثم"/"و") ---------- */
function parseCommandPart(part, context = {}) {
// تطبيع مبدئي
const raw = part;
const input = normalizeInputRaw(raw);
const action = {
intent: null, // sum, copy, delete, color...
target: null, // cell, row, column, horizontalColumn, verticalColumn
index: null, // رقم أو "last"
position: null, // after|before|at
color: null, // red, blue...
condition: null, // contains|equals
conditionValue: null, // نص الشرط
references: [], // قائمة مراجع موجودة في الجزء [{ type: 'lastResult', value: context.lastResult }]
useReference: false, // هل استخدم "مثل/زي" للإشارة لأخذ قيمة من مرجع
raw: raw
};
// 1) intent (أقوى مرادفات)
action.intent = findKeyByPhrase(input, "intents");
// 2) target (خلية/صف/عمود/عمود أفقي/عمود عمودي)
action.target = findKeyByPhrase(input, "targets");
// 3) index (رقم العمود/الصف)
action.index = findKeyByPhrase(input, "numbers");
// 4) position (بعد/قبل/في)
action.position = findKeyByPhrase(input, "positions");
// 5) color
action.color = findKeyByPhrase(input, "colors");
// 6) condition (تحتوي على / مكتوب فيها / يساوي)
// نبحث باستخدام findAllKeysByPhrase لأن قد توجد تعدد
const condKey = findKeyByPhrase(input, "conditions");
if (condKey) {
action.condition = condKey;
// نأخذ قيمة الشرط: كل النص بعد عبارة الشرط حتى نهاية الـ part أو حتى كلمة ربط (لكن هنا part مفصول)
// نبحث أي مرادف من config.conditions[condKey] ونأخذ ما بعده
const phrases = config.conditions[condKey];
for (const ph of phrases) {
const idx = input.indexOf(ph);
if (idx !== -1) {
const after = raw.slice(idx + ph.length).trim();
// نزيل كلمات ربط زائدة في البداية مثل "الـ" أو "النص" إن وُجدت
action.conditionValue = after.replace(/^(النص|النص\s+ال)?/i, '').trim();
break;
}
}
}
// 7) references الموجودة صراحة في الجزء
const refsFound = findAllKeysByPhrase(input, "references");
for (const r of refsFound) {
action.references.push({
type: r,
value: context[r] || null
});
}
// 8) استخدام مرجع عبر كلمات مثل "مثل، زي، كما" -> useReference
for (const w of (config.referenceWords || [])) {
if (includesPhraseAnywhere(input, w)) {
action.useReference = true;
break;
}
}
// 9) إذا لم يذكر target لكن intent يستهدف مرجع (مثل "انسخه" بعد بحث)، نعطي target من المرجع
// هذا يساعد حالات: "ابحث عن احمد ثم انسخه" => copy target = lastSearch (لو متوفر)
if (!action.target && action.intent) {
// أمثلة: "انسخه" عادة تتبع "search" أو "lastSearch"
if (["copy", "cut", "color", "insert", "delete", "clear"].includes(action.intent)) {
// نقرر أفضل مرجع من السياق: lastSearch ثم lastResult ثم lastColumn ثم lastSelection
if (!action.references.length) {
const prefer = ["lastSearch", "lastResult", "lastColumn", "lastSelection"];
for (const p of prefer) {
if (context[p]) {
action.references.push({ type: p, value: context[p] });
break;
}
}
}
// لو أخذنا مرجع ونستطيع تحديد target تلقائياً:
if (action.references.length) {
// ضع target كمرجع منطقي (مثلاً lastSearch → treat as 'search result')
action.target = action.references[0].type; // ملاحظة: caller يمكنه تفسير هذا
}
}
}
// 10) تنظيف بعض القيم: تحول number key إلى int أو 'last'
if (action.index && !isNaN(action.index)) {
action.index = parseInt(action.index, 10);
}
return action;
}
/* ---------- تجزئة الجملة إلى أجزاء (ثم/و) + تحليل كل جزء مع context handling ---------- */
function splitToParts(sentence) {
// نجزئ حسب "ثم" و "و" لكن نحاول عدم تقسيم داخل شروط (تبسيط)
// نستخدم تعبير منتظم يسقط كلمات الربط المنفصلة
return sentence.split(/\s+(?:ثم|و)\s+/i).map(p => p.trim()).filter(Boolean);
}
function parseSentenceToActions(sentence) {
// تطبيع كامل الجملة
const rawSentence = sentence.trim();
// لو أردت استخدام Araby.js: rawSentence = normalize(stripTashkeel(rawSentence));
const parts = splitToParts(rawSentence);
const context = {
lastResult: null,
lastColumn: null,
lastSearch: null,
lastSelection: null
};
const actions = [];
for (const part of parts) {
const act = parseCommandPart(part, context);
if (!act || !act.intent) continue;
// إذا الفعل يعتمد على مرجع و useReference true، ضف المرجع من السياق إن وُجد
if (act.useReference && !act.references.length) {
// اولاً نبحث عن lastResult ثم lastSearch ثم lastColumn ثم lastSelection
for (const k of ["lastResult", "lastSearch", "lastColumn", "lastSelection"]) {
if (context[k]) {
act.references.push({ type: k, value: context[k] });
break;
}
}
}
// تحويل حالات خاصة: لو intent = copy و فيها نص "ضعه بعد العمود X" نكسرها لعمليتين: copy + insert
// لكن هنا سنبقي كل act كما هو؛ المحرك التنفيذي (executor) يمكنه تفسير insert في نفس act
// مثلاً: act.intent = "copy", act.position = "after", act.index = 3
actions.push(act);
// تحديث السياق: قواعد بسيطة داخل الكود (كما طلبت سابقًا)
// قواعد: search => lastSearch, أي عملية حسابية/نسخ/قص => lastResult, تحديد عمود => lastColumn, select => lastSelection
if (act.intent === "search") {
context.lastSearch = act;
}
if (["sum", "subtract", "multiply", "divide", "copy", "cut", "average", "max", "min", "insert"].includes(act.intent)) {
context.lastResult = act;
}
if (act.target === "column" || act.target === "horizontalColumn" || act.target === "verticalColumn") {
if (act.index != null) context.lastColumn = act.index;
}
if (act.intent === "select") {
context.lastSelection = act;
}
// إذا الشرط أخلى الaction يحتوي على referenceValue (مثلاً conditionValue مقصودة)
// لا نغير context هنا؛ التنفيذ الفعلي سيحدّث النتيجة.
}
return { actions, context };
}
/* ---------- أمثلة اختبارية (جمل من المحادثة) ---------- */
const examples = [
"اجمع الأرقام في العمود الاول مع العمود الثاني و ضع النتيجة في اخر عمود",
"اطرح العمود الرابع من العمود الثاني",
"انسخ العمود الثالث وضعه بعد العمود السادس",
"قص العمود الخامس",
"ابحث عن كلمة احمد ولونها بالاخضر",
"حدد العمود العشرون",
"انسخ العمود الأول ثم اجمعه مع العمود الثاني ولونه بالأصفر",
"ابحث عن كلمة احمد ثم انسخه ثم اجمعه مع العمود الرابع",
"اجعل جميع المراجع داخل ملف الجايسون", // نص عادي لا ينطوي على أوامر تنفيذية هنا ولكنه سيُتفادى
"احذف الخلية التي تحتوي على النص جميل ثم لون العمود الأفقي الرابع بالأصفر",
"احذف الخلية التي مكتوب فيها جميل",
"احذف العمود الافقي الرابع",
"لون العمود العمودي الرابع بالاصفر",
"احذف الصف العمودي الخامس",
"ابحث عن احمد ثم انسخه وضعه بعد العمود الثالث ثم لونه مثل النتيجة السابقة"
];
console.log("=== Parsing examples and printing actions ===\n");
for (const ex of examples) {
const res = parseSentenceToActions(ex);
console.log("SENTENCE:", ex);
console.log("ACTIONS:", JSON.stringify(res.actions, null, 2));
console.log("CONTEXT_AFTER:", JSON.stringify(res.context, null, 2));
console.log("---------------------------------------------------\n");
}
/* ---------- ملاحظات للتنفيذ في تطبيق حقيقي ----------
- Executor: بعد الحصول على `actions` يجب أن تترجم كل action إلى عمليات فعلية على الـ spreadsheet.
مثال: { intent: "delete", target: "cell", condition: "contains", conditionValue: "جميل" }
=> ابحث عن الخلايا التي تحتوي "جميل" ثم امسحها.
- التعامل مع "مثل/useReference": لو action.useReference صحيح ولدى action.references عنصر،
فمثلاً color.useReference -> يعني "استخدم نفس اللون من المرجع"؛ لذا Executor يبحث عن المرجع (action.references[0])
ويستخرج خاصية اللون منه أو يستعمل النتيجة المتاحة.
- تحسينات مستقبلية:
* استخدام Araby.js للتطبيع الأفضل (stemming, tashkeel removal)
* كشف ضمائر أكثر (اجمعه، اجعلها...) وتحويلها إلى مراجع واضحة (pronominal resolution)
* دعم صيغ عددية أكثر مرونة (عمود 12, العمود ١٢, A, B)
* توسيع config لإضافة المزيد من الكلمات/اللغات
------------------------------------------------------------------- */
هذا هو ملف commands.json الكامل (يشمل جميع المرادفات والألوان والأرقام والوظائف كما ناقشنا سابقًا).
يمكنك نسخه مباشرة وحفظه باسم commands.json في مشروعك:
{
"intents": {
"sum": ["اجمع", "اضف", "احسب", "مجموع"],
"subtract": ["اطرح", "اخصم", "انقص", "اطرحه"],
"multiply": ["اضرب", "ضاعف", "اضرب في"],
"divide": ["اقسم", "قسمة", "اقسمه"],
"copy": ["انسخ", "انقل", "انسخه", "انسخها"],
"cut": ["اقطع", "قص", "قصه", "اقطعها"],
"insert": ["ضع", "ادرج", "حط", "ضعه"],
"search": ["ابحث", "ابحث عن", "جد", "فتش"],
"color": ["لون", "خليه", "غير اللون", "لونه", "لونها"],
"resize": ["كبر", "صغر", "غير الحجم"],
"select": ["حدد", "اختر", "تحديد"],
"delete": ["احذف", "ازل", "امسح", "امسحها", "امسح الخلية"],
"clear": ["افرغ", "امسح المحتوى"]
},
"colors": {
"red": ["احمر", "الأحمر", "احمرًا", "بالاحمر", "بالأحمر"],
"blue": ["ازرق", "الأزرق", "بالازرق", "بالأزرق"],
"green": ["اخضر", "الأخضر", "بالاخضر", "بالأخضر"],
"yellow": ["اصفر", "الأصفر", "بالاصفر"],
"black": ["اسود", "الأسود", "بالاسود"],
"white": ["ابيض", "الأبيض", "بالأبيض"],
"orange": ["برتقالي", "البرتقالي"],
"purple": ["بنفسجي", "ارجواني"],
"pink": ["وردي", "زهري"],
"gray": ["رمادي", "رصاصي"],
"brown": ["بني", "البني"],
"cyan": ["سماوي", "تركوازي"],
"gold": ["ذهبي", "الذهبي"],
"silver": ["فضي", "الفضي"]
},
"numbers": {
"1": ["اول", "الأول", "١", "واحد", "1"],
"2": ["ثاني", "الثاني", "٢", "اثنين", "2"],
"3": ["ثالث", "الثالث", "٣", "ثلاثة", "3"],
"4": ["رابع", "الرابع", "٤", "اربعة", "4"],
"5": ["خامس", "الخامس", "٥", "خمسة", "5"],
"6": ["سادس", "السادس", "٦", "ستة", "6"],
"7": ["سابع", "السابع", "٧", "سبعة", "7"],
"8": ["ثامن", "الثامن", "٨", "ثمانية", "8"],
"9": ["تاسع", "التاسع", "٩", "تسعة", "9"],
"10": ["عاشر", "العاشر", "١٠", "عشرة", "10"],
"11": ["الحادي عشر", "١١", "11"],
"12": ["الثاني عشر", "١٢", "12"],
"13": ["الثالث عشر", "١٣", "13"],
"14": ["الرابع عشر", "١٤", "14"],
"15": ["الخامس عشر", "١٥", "15"],
"16": ["السادس عشر", "١٦", "16"],
"17": ["السابع عشر", "١٧", "17"],
"18": ["الثامن عشر", "١٨", "18"],
"19": ["التاسع عشر", "١٩", "19"],
"20": ["العشرون", "٢٠", "20"],
"last": ["اخر", "الأخير", "النهاية"]
},
"positions": {
"after": ["بعد", "خلف"],
"before": ["قبل", "قدام"],
"at": ["في", "داخل", "عند"]
},
"targets": {
"cell": ["خلية", "الخانة"],
"row": ["صف", "السطر"],
"column": ["عمود", "كولون", "الكولون"],
"horizontalColumn": ["عمود افقي", "العمود الأفقي", "الافقي", "أفقي", "أفقيًا"],
"verticalColumn": ["عمود عمودي", "العمود العمودي", "العمودي", "عامودي", "عمودي"]
},
"conditions": {
"contains": ["تحتوي على", "فيها", "مكتوب فيها", "تضم", "تحتوي"],
"equals": ["يساوي", "مطابق", "هو", "يعادل", "يساوي بالضبط"]
},
"references": {
"lastResult": ["النتيجة", "الناتج", "output", "السابقة", "اللي فاتت"],
"lastSearch": ["البحث", "المطابقة", "النتائج", "نتائج البحث"],
"lastColumn": ["العمود", "الكولون", "العمود الذي ذكر", "العمود السابق"],
"lastSelection": ["التحديد", "المحدد", "الجزء المختار"]
},
"referenceWords": ["مثل", "زي", "كما", "مثلها", "نفس"]
}
هل تحب أن أضيف أيضًا حروف الأعمدة مثل (A, B, C...) داخل نفس ملف الجايسون عشان يدعم أوامر مثل "انسخ العمود B"؟