ObsiViewer/server/markdown-frontmatter.mjs

134 lines
3.9 KiB
JavaScript

#!/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 [];
}