Script pour déplacer des articles
This commit is contained in:
202
tools/lib/article_move.js
Normal file
202
tools/lib/article_move.js
Normal file
@@ -0,0 +1,202 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user