Normalisation des dates/heures
This commit is contained in:
@@ -417,7 +417,7 @@ async function main() {
|
|||||||
const coverLine = coverFile ? `cover: images/${coverFile}\n` : '';
|
const coverLine = coverFile ? `cover: images/${coverFile}\n` : '';
|
||||||
const md = `---\n` +
|
const md = `---\n` +
|
||||||
`title: "${pageTitle.replace(/"/g, '\\"')}"\n` +
|
`title: "${pageTitle.replace(/"/g, '\\"')}"\n` +
|
||||||
`date: ${createdAt}\n` +
|
`date: "${createdAt}"\n` +
|
||||||
coverLine +
|
coverLine +
|
||||||
`---\n\n` +
|
`---\n\n` +
|
||||||
body;
|
body;
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ if (duplicateBundlePath) {
|
|||||||
let content = fs.readFileSync(indexPath, "utf8");
|
let content = fs.readFileSync(indexPath, "utf8");
|
||||||
|
|
||||||
// Inject date
|
// Inject date
|
||||||
content = content.replace(/^date: .*/m, `date: ${formattedEntryDate}`);
|
content = content.replace(/^date: .*/m, `date: "${formattedEntryDate}"`);
|
||||||
|
|
||||||
// Inject status
|
// Inject status
|
||||||
const statusEntry = `{"date": "${formattedStatusDate}", "http_code": ${metadata.httpStatus || "null"}}`;
|
const statusEntry = `{"date": "${formattedStatusDate}", "http_code": ${metadata.httpStatus || "null"}}`;
|
||||||
|
|||||||
@@ -32,6 +32,57 @@ function getHugoTimeZone() {
|
|||||||
return cachedTimeZone;
|
return cachedTimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse une chaîne de date selon les formats Hugo attendus.
|
||||||
|
* @param {string} value Chaîne de date.
|
||||||
|
* @param {string} zone Fuseau horaire IANA.
|
||||||
|
* @param {number} defaultHour Heure par défaut si absente.
|
||||||
|
* @param {number} defaultMinute Minute par défaut si absente.
|
||||||
|
* @returns {import("luxon").DateTime|null} DateTime ou null si invalide.
|
||||||
|
*/
|
||||||
|
function parseHugoDateString(value, zone, defaultHour = 12, defaultMinute = 0) {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
||||||
|
(trimmed.startsWith("\"") && trimmed.endsWith("\""))
|
||||||
|
) {
|
||||||
|
trimmed = trimmed.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const iso = DateTime.fromISO(trimmed, { setZone: true });
|
||||||
|
if (iso.isValid) {
|
||||||
|
return iso.setZone(zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formats = [
|
||||||
|
"yyyy-LL-dd HH:mm:ss",
|
||||||
|
"yyyy-LL-dd'T'HH:mm:ss",
|
||||||
|
"yyyy-LL-dd HH:mm",
|
||||||
|
"yyyy-LL-dd'T'HH:mm",
|
||||||
|
"yyyy-LL-dd",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const format of formats) {
|
||||||
|
const parsed = DateTime.fromFormat(trimmed, format, { zone });
|
||||||
|
if (parsed.isValid) {
|
||||||
|
if (format === "yyyy-LL-dd") {
|
||||||
|
return parsed.set({ hour: defaultHour, minute: defaultMinute, second: 0, millisecond: 0 });
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rfc2822 = DateTime.fromRFC2822(trimmed, { setZone: true });
|
||||||
|
if (rfc2822.isValid) {
|
||||||
|
return rfc2822.setZone(zone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convertit une valeur vers un DateTime positionné sur le fuseau horaire Hugo.
|
* Convertit une valeur vers un DateTime positionné sur le fuseau horaire Hugo.
|
||||||
* @param {Date|import("luxon").DateTime|string|number|null} value Valeur à convertir (null : maintenant).
|
* @param {Date|import("luxon").DateTime|string|number|null} value Valeur à convertir (null : maintenant).
|
||||||
@@ -65,9 +116,9 @@ function toHugoDateTime(value = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const parsed = DateTime.fromISO(value, { setZone: true }).setZone(zone);
|
const parsed = parseHugoDateString(value, zone);
|
||||||
if (!parsed.isValid) {
|
if (!parsed) {
|
||||||
throw new Error(parsed.invalidReason || `Chaîne de date invalide : ${value}`);
|
throw new Error(`Chaîne de date invalide : ${value}`);
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@@ -84,14 +135,14 @@ function toHugoDateTime(value = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formate une date en ISO 8601 avec l'offset du fuseau horaire Hugo.
|
* Formate une date selon le format Hugo simple (sans offset).
|
||||||
* @param {Date|import("luxon").DateTime|string|number|null} value Valeur à formater.
|
* @param {Date|import("luxon").DateTime|string|number|null} value Valeur à formater.
|
||||||
* @returns {string} Timestamp ISO 8601 avec offset.
|
* @returns {string} Date formatée.
|
||||||
*/
|
*/
|
||||||
function formatDateTime(value = null) {
|
function formatDateTime(value = null) {
|
||||||
const zoned = toHugoDateTime(value);
|
const zoned = toHugoDateTime(value);
|
||||||
const normalized = zoned.set({ millisecond: 0 });
|
const normalized = zoned.set({ millisecond: 0 });
|
||||||
const formatted = normalized.toISO({ suppressMilliseconds: true });
|
const formatted = normalized.toFormat("yyyy-LL-dd HH:mm:ss");
|
||||||
|
|
||||||
if (!formatted) {
|
if (!formatted) {
|
||||||
throw new Error("Impossible de formater la date avec le fuseau Hugo.");
|
throw new Error("Impossible de formater la date avec le fuseau Hugo.");
|
||||||
@@ -119,16 +170,8 @@ function parseFrontmatterDate(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
const trimmed = value.trim();
|
const parsed = parseHugoDateString(value, zone);
|
||||||
if (!trimmed) {
|
return parsed;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const iso = DateTime.fromISO(trimmed, { setZone: true }).setZone(zone);
|
|
||||||
if (iso.isValid) {
|
|
||||||
return iso;
|
|
||||||
}
|
|
||||||
const rfc2822 = DateTime.fromRFC2822(trimmed, { setZone: true }).setZone(zone);
|
|
||||||
return rfc2822.isValid ? rfc2822 : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
|
|||||||
125
tools/normalize_article_dates.js
Normal file
125
tools/normalize_article_dates.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require("fs/promises");
|
||||||
|
const path = require("path");
|
||||||
|
const {
|
||||||
|
extractRawDate,
|
||||||
|
readFrontmatter,
|
||||||
|
writeFrontmatter,
|
||||||
|
} = require("./lib/weather/frontmatter");
|
||||||
|
const { parseFrontmatterDate, formatDateTime, getHugoTimeZone } = require("./lib/datetime");
|
||||||
|
|
||||||
|
const CONTENT_ROOT = path.join(process.cwd(), "content");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste récursivement tous les fichiers Markdown d'un dossier.
|
||||||
|
* @param {string} root Dossier racine à parcourir.
|
||||||
|
* @returns {Promise<string[]>} Chemins absolus des fichiers trouvés.
|
||||||
|
*/
|
||||||
|
async function listMarkdownFiles(root) {
|
||||||
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(root, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const nested = await listMarkdownFiles(fullPath);
|
||||||
|
results.push(...nested);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||||
|
results.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Harmonise la date d'un fichier Markdown si nécessaire.
|
||||||
|
* @param {string} filePath Chemin absolu du fichier.
|
||||||
|
* @returns {Promise<"updated"|"unchanged"|"skipped"|"invalid">} Statut de traitement.
|
||||||
|
*/
|
||||||
|
async function normalizeFileDate(filePath) {
|
||||||
|
const frontmatter = await readFrontmatter(filePath);
|
||||||
|
if (!frontmatter) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawDate = extractRawDate(frontmatter.frontmatterText);
|
||||||
|
const dateValue = frontmatter.doc.get("date");
|
||||||
|
|
||||||
|
if (!rawDate && (dateValue === undefined || dateValue === null)) {
|
||||||
|
return "skipped";
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceValue = rawDate !== null ? rawDate : dateValue;
|
||||||
|
const parsed = parseFrontmatterDate(sourceValue);
|
||||||
|
if (!parsed) {
|
||||||
|
return "invalid";
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasTime = false;
|
||||||
|
if (typeof rawDate === "string") {
|
||||||
|
const timeMatch = rawDate.match(/[T ](\d{2}):(\d{2})(?::(\d{2}))?/);
|
||||||
|
if (timeMatch) {
|
||||||
|
const hour = Number(timeMatch[1]);
|
||||||
|
const minute = Number(timeMatch[2]);
|
||||||
|
const second = Number(timeMatch[3] || "0");
|
||||||
|
hasTime = !(hour === 0 && minute === 0 && second === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const normalized = hasTime
|
||||||
|
? parsed.set({ millisecond: 0 })
|
||||||
|
: parsed.set({ hour: 12, minute: 0, second: 0, millisecond: 0 });
|
||||||
|
const formatted = formatDateTime(normalized);
|
||||||
|
const current = typeof dateValue === "string" ? dateValue.trim() : rawDate;
|
||||||
|
const quotedFormatted = `"${formatted}"`;
|
||||||
|
const currentComparable = typeof current === "string" ? current.trim() : "";
|
||||||
|
|
||||||
|
if (currentComparable === formatted || currentComparable === quotedFormatted) {
|
||||||
|
return "unchanged";
|
||||||
|
}
|
||||||
|
|
||||||
|
frontmatter.doc.set("date", formatted);
|
||||||
|
await writeFrontmatter(filePath, frontmatter.doc, frontmatter.body);
|
||||||
|
const rewritten = await fs.readFile(filePath, "utf8");
|
||||||
|
const normalizedContent = rewritten.replace(/^date:\s*.+$/m, `date: ${quotedFormatted}`);
|
||||||
|
if (rewritten !== normalizedContent) {
|
||||||
|
await fs.writeFile(filePath, normalizedContent, "utf8");
|
||||||
|
}
|
||||||
|
return "updated";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point d'entrée du script.
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const timezone = getHugoTimeZone();
|
||||||
|
console.log(`Fuseau horaire Hugo : ${timezone}`);
|
||||||
|
|
||||||
|
const files = await listMarkdownFiles(CONTENT_ROOT);
|
||||||
|
let updated = 0;
|
||||||
|
let unchanged = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let invalid = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const status = await normalizeFileDate(file);
|
||||||
|
if (status === "updated") updated += 1;
|
||||||
|
else if (status === "unchanged") unchanged += 1;
|
||||||
|
else if (status === "invalid") {
|
||||||
|
invalid += 1;
|
||||||
|
const relative = path.relative(process.cwd(), file);
|
||||||
|
console.warn(`Date invalide : ${relative}`);
|
||||||
|
} else {
|
||||||
|
skipped += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Terminé. ${updated} mis à jour, ${unchanged} inchangés, ${skipped} ignorés, ${invalid} invalides.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user