server / services /ShortcodeService.js
Mark-Lasfar
feat(seo): add complete dynamic sitemap system with posts, jobs, templates, pages, tags, images, and comments
bee235c
Raw
History Blame Contribute Delete
7.86 kB
const StoreShortcode = require("../models/StoreShortcode");
/**
* خدمة معالجة الأكواد المختصرة
* @version 2.0.0
*/
class ShortcodeService {
/**
* معالجة النص واستبدال جميع الـ Shortcodes
* @param {string} content - المحتوى الأصلي
* @param {string} storeId - معرف المتجر
* @param {Object} options - خيارات إضافية
* @returns {Promise<string>} - المحتوى بعد المعالجة
*/
static async parseShortcodes(content, storeId, options = {}) {
if (!content || typeof content !== 'string') return content;
// التعبير النمطي للـ Shortcodes
const shortcodeRegex = /\[(\w+)(?:\s+([^\]]*))?\]/g;
let parsedContent = content;
let match;
// تجميع جميع الـ Shortcodes الفريدة
const shortcodesToProcess = new Map();
while ((match = shortcodeRegex.exec(content)) !== null) {
const name = match[1];
const attrsStr = match[2] || '';
if (!shortcodesToProcess.has(name)) {
shortcodesToProcess.set(name, []);
}
shortcodesToProcess.get(name).push({
fullMatch: match[0],
attrs: this.parseAttributes(attrsStr)
});
}
// معالجة كل Shortcode
for (const [name, occurrences] of shortcodesToProcess) {
const shortcode = await StoreShortcode.findOne({
userId: storeId,
name: name,
isActive: true
});
if (!shortcode) continue;
for (const occ of occurrences) {
try {
const html = await shortcode.render(occ.attrs, {
storeId,
userToken: options.userToken
});
parsedContent = parsedContent.replace(occ.fullMatch, html);
} catch (error) {
console.error(`Error processing shortcode ${name}:`, error);
parsedContent = parsedContent.replace(occ.fullMatch,
`<div class="shortcode-error text-red-500 p-2 border border-red-300 rounded">
Error: ${error.message}
</div>`
);
}
}
}
return parsedContent;
}
/**
* تحليل معاملات الـ Shortcode
* @param {string} attrsStr - نص المعاملات
* @returns {Object} - كائن المعاملات
*/
static parseAttributes(attrsStr) {
const attrs = {};
if (!attrsStr) return attrs;
// دعم كل من name="value" و name=value
const attrRegex = /(\w+)(?:="([^"]*)"|='([^']*)'|="([^"]*)"|="([^"]*)"|="([^"]*)"|=([^\s]+))/g;
let match;
while ((match = attrRegex.exec(attrsStr)) !== null) {
const name = match[1];
let value = match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || '';
// تحويل القيم إلى الأنواع المناسبة
if (value === 'true') value = true;
else if (value === 'false') value = false;
else if (!isNaN(value) && value !== '') value = Number(value);
attrs[name] = value;
}
return attrs;
}
/**
* الحصول على قائمة جميع الـ Shortcodes المتاحة لمستخدم
* @param {string} userId - معرف المستخدم
* @returns {Promise<Array>} - قائمة الـ Shortcodes
*/
static async getAllShortcodes(userId) {
const builtins = StoreShortcode.getBuiltinShortcodes();
const custom = await StoreShortcode.find({ userId, type: 'custom', isActive: true });
return [
...builtins.map(b => ({ ...b, isBuiltin: true })),
...custom.map(c => ({ ...c.toObject(), isBuiltin: false }))
];
}
/**
* إنشاء Shortcode مخصص جديد
* @param {string} userId - معرف المستخدم
* @param {Object} data - بيانات الـ Shortcode
* @returns {Promise<Object>} - الـ Shortcode المنشأ
*/
static async createCustomShortcode(userId, data) {
const { name, displayName, description, template, parameters, settings } = data;
// التحقق من عدم وجود اسم مكرر
const existing = await StoreShortcode.findOne({ userId, name });
if (existing) {
throw new Error(`Shortcode "${name}" already exists`);
}
const shortcode = new StoreShortcode({
userId,
name: name.toLowerCase().replace(/[^a-z0-9_-]/g, ''),
displayName,
description,
template,
parameters: parameters || [],
settings: settings || {},
type: 'custom'
});
await shortcode.save();
return shortcode;
}
/**
* تحديث Shortcode مخصص
* @param {string} shortcodeId - معرف الـ Shortcode
* @param {string} userId - معرف المستخدم (للتحقق)
* @param {Object} data - بيانات التحديث
* @returns {Promise<Object>} - الـ Shortcode المحدث
*/
static async updateCustomShortcode(shortcodeId, userId, data) {
const shortcode = await StoreShortcode.findOne({ _id: shortcodeId, userId });
if (!shortcode) {
throw new Error('Shortcode not found');
}
Object.assign(shortcode, data);
shortcode.cachedOutput = null; // إبطال التخزين المؤقت
await shortcode.save();
return shortcode;
}
/**
* حذف Shortcode مخصص
* @param {string} shortcodeId - معرف الـ Shortcode
* @param {string} userId - معرف المستخدم
* @returns {Promise<boolean>}
*/
static async deleteCustomShortcode(shortcodeId, userId) {
const result = await StoreShortcode.deleteOne({ _id: shortcodeId, userId, type: 'custom' });
return result.deletedCount > 0;
}
/**
* معاينة Shortcode (دون حفظ)
* @param {Object} data - بيانات الـ Shortcode
* @param {Object} context - السياق
* @returns {Promise<string>} - HTML المعاينة
*/
static async previewShortcode(data, context) {
const { name, displayName, template, parameters, attrs = {} } = data;
// دمج المعاملات مع القيم الافتراضية
const mergedAttrs = {};
if (parameters) {
for (const param of parameters) {
mergedAttrs[param.name] = attrs[param.name] !== undefined
? attrs[param.name]
: param.defaultValue;
}
}
// تنفيذ القالب
let html = template || '';
for (const [key, value] of Object.entries(mergedAttrs)) {
const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
html = html.replace(regex, String(value ?? ''));
}
return html || `<div class="p-4 text-gray-500">Preview: ${displayName} (no content)</div>`;
}
/**
* تهيئة الـ Shortcodes لمستخدم جديد
* @param {string} userId - معرف المستخدم
*/
static async initializeUserShortcodes(userId) {
await StoreShortcode.initializeBuiltins(userId);
}
}
module.exports = ShortcodeService;