#!/usr/bin/env node /** * Markdown front-matter utilities for tag management * Handles YAML front-matter parsing and serialization with strict business rules */ /** * Normalise un tag : trim, normalise les espaces */ function normalizeTag(tag) { return String(tag || '').trim().replace(/\s+/g, ' '); } /** * Déduplique les tags (case-insensitive) en préservant la première occurrence */ function deduplicateTags(tags) { const seen = new Set(); const result = []; for (const tag of tags) { const normalized = normalizeTag(tag); if (!normalized) continue; const lower = normalized.toLowerCase(); if (!seen.has(lower)) { seen.add(lower); result.push(normalized); } } return result; } /** * Réécrit le front-matter YAML d'un fichier Markdown avec les tags fournis. * * Règles métier : * - Crée la section --- ... --- si absente * - Normalise et déduplique les tags (case-insensitive) * - Si liste vide, supprime la clé tags: (ne laisse pas tags: []) * - Aucune ligne vide dans le front-matter résultant * - Préserve l'ordre et le format des autres propriétés * * @param {string} rawMarkdown - Contenu Markdown brut * @param {string[]} tags - Liste des tags à appliquer * @returns {string} - Contenu Markdown mis à jour */ export function rewriteTagsFrontmatter(rawMarkdown, tags) { // Normaliser le contenu (BOM, line endings) const content = rawMarkdown.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n'); // Détecter le front-matter existant const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/); // Normaliser et dédupliquer les tags const cleanTags = deduplicateTags(tags || []); // Construire le bloc tags (format YAML liste) const buildTagsBlock = (tagList) => { if (tagList.length === 0) return ''; // Ne pas créer de clé tags si vide return 'tags:\n' + tagList.map(t => ` - ${t}`).join('\n'); }; const newTagsBlock = buildTagsBlock(cleanTags); // Cas 1 : Pas de front-matter existant if (!fmMatch) { if (cleanTags.length === 0) { // Pas de tags, pas de front-matter à créer return content; } // Créer un nouveau front-matter avec les tags return `---\n${newTagsBlock}\n---\n${content}`; } // Cas 2 : Front-matter existant const fmText = fmMatch[1]; const body = fmMatch[2] || ''; // Supprimer l'ancien bloc tags (format liste ou inline) const tagsRe = /(^|\n)tags\s*:[^\n]*(?:\n\s+-\s+[^\n]*)*(?=\n|$)/i; let updatedFm = fmText.replace(tagsRe, ''); // Nettoyer les lignes vides multiples updatedFm = updatedFm.replace(/\n{2,}/g, '\n').trim(); // Ajouter le nouveau bloc tags si non vide if (newTagsBlock) { updatedFm = updatedFm ? `${updatedFm}\n${newTagsBlock}` : newTagsBlock; } // Si le front-matter est vide après suppression, ne pas créer de section vide if (!updatedFm.trim()) { return body; } // Reconstruire le document return `---\n${updatedFm}\n---\n${body}`; } /** * Extrait les tags du front-matter YAML * @param {string} rawMarkdown - Contenu Markdown brut * @returns {string[]} - Liste des tags trouvés */ export function extractTagsFromFrontmatter(rawMarkdown) { const content = rawMarkdown.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n'); const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!fmMatch) return []; const fmText = fmMatch[1]; // Chercher le bloc tags (format liste) const tagsListMatch = fmText.match(/^tags:\s*\n((?:\s+-\s+.+\n?)+)/m); if (tagsListMatch) { const lines = tagsListMatch[1].split('\n').filter(Boolean); return lines.map(line => line.replace(/^\s*-\s*/, '').trim()).filter(Boolean); } // Chercher format inline (tags: [tag1, tag2]) const tagsInlineMatch = fmText.match(/^tags:\s*\[(.*?)\]/m); if (tagsInlineMatch) { return tagsInlineMatch[1] .split(',') .map(t => t.trim()) .filter(Boolean); } return []; }