1
Files
2025/tools/lib/article_move.js

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,
};