seogeneratorserver/lib/Utils.js
StillHammer dbf1a3de8c Add technical plan for multi-format export system
Added plan.md with complete architecture for format-agnostic content generation:
- Support for Markdown, HTML, Plain Text, JSON formats
- New FormatExporter module with neutral data structure
- Integration strategy with existing ContentAssembly and ArticleStorage
- Bonus features: SEO metadata generation, readability scoring, WordPress Gutenberg format
- Implementation roadmap with 4 phases (6h total estimated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:14:29 +08:00

273 lines
7.7 KiB
JavaScript

// ========================================
// FICHIER: utils.js - Conversion Node.js
// Description: Utilitaires génériques pour le workflow
// ========================================
// Import du système de logging (assumant que logSh est disponible globalement)
// const { logSh } = require('./logging'); // À décommenter si logSh est dans un module séparé
/**
* Créer une réponse de succès standardisée
* @param {Object} data - Données à retourner
* @returns {Object} Réponse formatée pour Express/HTTP
*/
function createSuccessResponse(data) {
return {
success: true,
data: data,
timestamp: new Date().toISOString()
};
}
/**
* Créer une réponse d'erreur standardisée
* @param {string|Error} error - Message d'erreur ou objet Error
* @returns {Object} Réponse d'erreur formatée
*/
function createErrorResponse(error) {
const errorMessage = error instanceof Error ? error.message : error.toString();
return {
success: false,
error: errorMessage,
timestamp: new Date().toISOString(),
stack: process.env.NODE_ENV === 'development' && error instanceof Error ? error.stack : undefined
};
}
/**
* Middleware Express pour envoyer des réponses standardisées
* Usage: res.success(data) ou res.error(error)
*/
function responseMiddleware(req, res, next) {
// Méthode pour réponse de succès
res.success = (data, statusCode = 200) => {
res.status(statusCode).json(createSuccessResponse(data));
};
// Méthode pour réponse d'erreur
res.error = (error, statusCode = 500) => {
res.status(statusCode).json(createErrorResponse(error));
};
next();
}
/**
* HELPER : Nettoyer les instructions FAQ
* Remplace les variables et nettoie le HTML
* @param {string} instructions - Instructions à nettoyer
* @param {Object} csvData - Données CSV pour remplacement variables
* @returns {string} Instructions nettoyées
*/
function cleanFAQInstructions(instructions, csvData) {
if (!instructions || !csvData) {
return instructions || '';
}
let clean = instructions.toString();
try {
// Remplacer variables simples
clean = clean.replace(/\{\{MC0\}\}/g, csvData.mc0 || '');
clean = clean.replace(/\{\{T0\}\}/g, csvData.t0 || '');
// Variables multiples si nécessaire
if (csvData.mcPlus1) {
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const mcValue = mcPlus1[i-1] || `[MC+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{MC\\+1_${i}\\}\\}`, 'g'), mcValue);
}
}
// Variables T+1 et L+1 si disponibles
if (csvData.tPlus1) {
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const tValue = tPlus1[i-1] || `[T+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{T\\+1_${i}\\}\\}`, 'g'), tValue);
}
}
if (csvData.lPlus1) {
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
for (let i = 1; i <= 6; i++) {
const lValue = lPlus1[i-1] || `[L+1_${i} non défini]`;
clean = clean.replace(new RegExp(`\\{\\{L\\+1_${i}\\}\\}`, 'g'), lValue);
}
}
// Nettoyer HTML
clean = clean.replace(/<\/?[^>]+>/g, '');
// Nettoyer espaces en trop
clean = clean.replace(/\s+/g, ' ').trim();
} catch (error) {
if (typeof logSh === 'function') {
logSh(`⚠️ Erreur nettoyage instructions FAQ: ${error.toString()}`, 'WARNING');
}
// Retourner au moins la version partiellement nettoyée
}
return clean;
}
/**
* Utilitaire pour attendre un délai (remplace Utilities.sleep de Google Apps Script)
* @param {number} ms - Millisecondes à attendre
* @returns {Promise} Promise qui se résout après le délai
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Utilitaire pour encoder en base64
* @param {string} text - Texte à encoder
* @returns {string} Texte encodé en base64
*/
function base64Encode(text) {
return Buffer.from(text, 'utf8').toString('base64');
}
/**
* Utilitaire pour décoder du base64
* @param {string} base64Text - Texte base64 à décoder
* @returns {string} Texte décodé
*/
function base64Decode(base64Text) {
return Buffer.from(base64Text, 'base64').toString('utf8');
}
/**
* Valider et nettoyer un slug/filename
* @param {string} slug - Slug à nettoyer
* @returns {string} Slug nettoyé
*/
function cleanSlug(slug) {
if (!slug) return '';
return slug
.toString()
.toLowerCase()
.replace(/[^a-z0-9\-_]/g, '-') // Remplacer caractères spéciaux par -
.replace(/-+/g, '-') // Éviter doubles tirets
.replace(/^-+|-+$/g, ''); // Enlever tirets début/fin
}
/**
* Compter les mots dans un texte
* @param {string} text - Texte à analyser
* @returns {number} Nombre de mots
*/
function countWords(text) {
if (!text || typeof text !== 'string') return 0;
return text
.trim()
.replace(/\s+/g, ' ') // Normaliser espaces
.split(' ')
.filter(word => word.length > 0)
.length;
}
/**
* Formater une durée en millisecondes en format lisible
* @param {number} ms - Durée en millisecondes
* @returns {string} Durée formatée (ex: "2.3s" ou "450ms")
*/
function formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
const minutes = Math.floor(ms / 60000);
const seconds = ((ms % 60000) / 1000).toFixed(1);
return `${minutes}m ${seconds}s`;
}
}
/**
* Utilitaire pour retry automatique d'une fonction
* @param {Function} fn - Fonction à exécuter avec retry
* @param {number} maxRetries - Nombre maximum de tentatives
* @param {number} delay - Délai entre tentatives (ms)
* @returns {Promise} Résultat de la fonction ou erreur finale
*/
async function withRetry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (typeof logSh === 'function') {
logSh(`⚠️ Tentative ${attempt}/${maxRetries} échouée: ${error.toString()}`, 'WARNING');
}
if (attempt < maxRetries) {
await sleep(delay * attempt); // Exponential backoff
}
}
}
throw lastError;
}
/**
* Validation basique d'email
* @param {string} email - Email à valider
* @returns {boolean} True si email valide
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Générer un ID unique simple
* @returns {string} ID unique basé sur timestamp + random
*/
function generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Truncate un texte à une longueur donnée
* @param {string} text - Texte à tronquer
* @param {number} maxLength - Longueur maximale
* @param {string} suffix - Suffixe à ajouter si tronqué (défaut: '...')
* @returns {string} Texte tronqué
*/
function truncate(text, maxLength, suffix = '...') {
if (!text || text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - suffix.length) + suffix;
}
// ============= EXPORTS =============
module.exports = {
createSuccessResponse,
createErrorResponse,
responseMiddleware,
cleanFAQInstructions,
sleep,
base64Encode,
base64Decode,
cleanSlug,
countWords,
formatDuration,
withRetry,
isValidEmail,
generateId,
truncate
};