203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
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,
|
|
};
|