const fs = require("node:fs"); const path = require("node:path"); const { readFrontmatterFile, writeFrontmatterFile } = require("./frontmatter"); /** * Résout un chemin source vers le dossier du bundle. * @param {string} input Chemin fourni par l'utilisateur. * @returns {string} Chemin absolu du bundle. */ function resolveBundlePath(input) { const resolved = path.resolve(input); if (resolved.toLowerCase().endsWith(`${path.sep}index.md`)) { return path.dirname(resolved); } return resolved; } /** * Vérifie la présence d'un bundle Hugo. * @param {string} bundleDir Chemin absolu du bundle. */ function ensureBundleExists(bundleDir) { if (!fs.existsSync(bundleDir)) { throw new Error(`Le bundle ${bundleDir} est introuvable.`); } const stats = fs.statSync(bundleDir); if (!stats.isDirectory()) { throw new Error(`Le bundle ${bundleDir} n'est pas un dossier.`); } const indexPath = path.join(bundleDir, "index.md"); if (!fs.existsSync(indexPath)) { throw new Error(`Le bundle ${bundleDir} ne contient pas index.md.`); } } /** * Vérifie que le chemin reste sous content/. * @param {string} targetPath Chemin absolu à contrôler. * @param {string} contentRoot Racine content/. */ function ensureWithinContent(targetPath, contentRoot) { const relative = path.relative(contentRoot, targetPath); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error(`Le chemin ${targetPath} est en dehors de content/.`); } } /** * Découpe un chemin de bundle en segments relatifs. * @param {string} bundleDir Chemin absolu du bundle. * @param {string} contentRoot Racine content/. * @returns {string[]} Segments relatifs. */ function splitRelativeParts(bundleDir, contentRoot) { const relative = path.relative(contentRoot, bundleDir); return relative.split(path.sep).filter(Boolean); } /** * Résout la destination finale en tenant compte de l'arborescence de dates. * @param {string} input Chemin de destination fourni. * @param {string} slug Slug du bundle source. * @param {{ segments: string[] }|null} sourceDate Segments de date de la source. * @param {string} contentRoot Racine content/. * @param {Function} isDateSegment Fonction de détection de date. * @returns {{ bundleDir: string }} Chemin final du bundle. */ function resolveDestination(input, slug, sourceDate, contentRoot, isDateSegment) { const resolved = path.resolve(input); let destinationDir = resolved; if (resolved.toLowerCase().endsWith(`${path.sep}index.md`)) { destinationDir = path.dirname(resolved); } let includesSlug = false; if (path.basename(destinationDir) === slug) { includesSlug = true; } let baseDir = destinationDir; if (includesSlug) { baseDir = path.dirname(destinationDir); } const destDate = findDateSegments(splitRelativeParts(baseDir, contentRoot), isDateSegment); if (sourceDate && !destDate) { baseDir = path.join(baseDir, ...sourceDate.segments); } let bundleDir = baseDir; if (!includesSlug) { bundleDir = path.join(baseDir, slug); } return { bundleDir }; } /** * Déplace un bundle dans sa nouvelle destination. * @param {string} sourceDir Chemin source. * @param {string} destinationDir Chemin cible. */ function moveBundle(sourceDir, destinationDir) { fs.mkdirSync(path.dirname(destinationDir), { recursive: true }); fs.renameSync(sourceDir, destinationDir); } /** * Ajoute un alias Hugo vers l'ancien chemin. * @param {string} bundleDir Chemin du bundle déplacé. * @param {string[]} oldParts Segments de l'ancien chemin relatif. */ function addAlias(bundleDir, oldParts) { const indexPath = path.join(bundleDir, "index.md"); const frontmatter = readFrontmatterFile(indexPath); if (!frontmatter) { throw new Error(`Frontmatter introuvable pour ${bundleDir}.`); } const alias = `/${oldParts.join("/")}/`; const aliases = normalizeAliases(frontmatter.data.aliases); if (!aliases.includes(alias)) { aliases.push(alias); } frontmatter.data.aliases = aliases; writeFrontmatterFile(indexPath, frontmatter.data, frontmatter.body); } /** * Normalise un champ aliases en tableau de chaînes. * @param {unknown} value Valeur brute du frontmatter. * @returns {string[]} Tableau nettoyé. */ function normalizeAliases(value) { const aliases = []; if (Array.isArray(value)) { for (const entry of value) { if (typeof entry === "string" && entry.trim()) { aliases.push(entry.trim()); } } return aliases; } if (typeof value === "string" && value.trim()) { aliases.push(value.trim()); } return aliases; } /** * Supprime les dossiers parents vides jusqu'à content/. * @param {string} startDir Dossier de départ. * @param {string} stopDir Dossier racine à préserver. */ function cleanupEmptyParents(startDir, stopDir) { let current = startDir; while (current.startsWith(stopDir)) { if (!fs.existsSync(current)) { current = path.dirname(current); continue; } const entries = fs.readdirSync(current); if (entries.length > 0) { return; } fs.rmdirSync(current); if (current === stopDir) { return; } current = path.dirname(current); } } /** * Détecte une arborescence de date dans un chemin. * @param {string[]} parts Segments à analyser. * @param {Function} isDateSegment Fonction de détection de date. * @returns {{ segments: string[] }|null} Segments de date ou null. */ function findDateSegments(parts, isDateSegment) { let index = 0; while (index < parts.length - 2) { const dateSegments = isDateSegment(parts, index); if (dateSegments) { return { segments: dateSegments }; } index += 1; } return null; } module.exports = { resolveBundlePath, ensureBundleExists, ensureWithinContent, splitRelativeParts, resolveDestination, moveBundle, addAlias, cleanupEmptyParents, findDateSegments, };