Ajout d'un script d'horodatage des fiches, affichage des fiches récement modifiées sur la page d'accueil

This commit is contained in:
julien
2026-08-05 17:46:30 +02:00
parent 2b8c088e57
commit 65642a5983
35 changed files with 279 additions and 60 deletions
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Horodate la dernière modification substantielle dune fiche."""
import argparse
import re
import sys
import tomllib
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from _schema import KNOWN_SECTIONS
ROOT_DIR = Path(__file__).resolve().parent.parent
CONTENT_DIR = ROOT_DIR / "src" / "content"
TIMEZONE_NAME = "Europe/Paris"
UPDATED_RE = re.compile(r"\s*(?:#\s*)?updated\s*=.*")
DATE_RE = re.compile(r"\s*date\s*=.*")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Horodater la dernière modification substantielle dune fiche."
)
parser.add_argument(
"fiche",
help=(
"chemin depuis la racine du dépôt ou depuis src/content/ "
"(par exemple vocabulaire/bītum.md)"
),
)
return parser.parse_args()
def current_timestamp() -> str:
try:
timezone = ZoneInfo(TIMEZONE_NAME)
except ZoneInfoNotFoundError as exc:
raise RuntimeError(
f"fuseau {TIMEZONE_NAME} indisponible ; installer les données de fuseaux horaires"
) from exc
return datetime.now(timezone).isoformat(timespec="seconds")
def resolve_content_path(value: str) -> Path:
supplied = Path(value).expanduser()
if supplied.is_absolute():
path = supplied
elif supplied.parts[:2] == ("src", "content"):
path = ROOT_DIR / supplied
else:
path = CONTENT_DIR / supplied
path = path.resolve()
try:
relative = path.relative_to(CONTENT_DIR.resolve())
except ValueError as exc:
raise ValueError("la fiche doit se trouver dans src/content/") from exc
if not path.is_file():
raise ValueError(f"fichier introuvable : {path}")
if path.suffix != ".md" or path.name == "_index.md":
raise ValueError("le chemin doit désigner une fiche Markdown, pas un index")
if len(relative.parts) < 2 or relative.parts[0] not in KNOWN_SECTIONS:
raise ValueError("la fiche doit appartenir à une section éditoriale connue")
return path
def update_front_matter(path: Path, timestamp: str) -> None:
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise ValueError(f"lecture impossible ({exc})") from exc
lines = text.splitlines(keepends=True)
if not lines or lines[0].rstrip("\r\n") != "+++":
raise ValueError("front matter TOML initial absent")
try:
closing_index = next(
index
for index, line in enumerate(lines[1:], start=1)
if line.rstrip("\r\n") == "+++"
)
except StopIteration as exc:
raise ValueError("délimiteur final +++ absent") from exc
try:
tomllib.loads("".join(lines[1:closing_index]))
except tomllib.TOMLDecodeError as exc:
raise ValueError(f"front matter TOML invalide ({exc})") from exc
updated_indexes = [
index
for index in range(1, closing_index)
if UPDATED_RE.fullmatch(lines[index].rstrip("\r\n")) is not None
]
if len(updated_indexes) > 1:
raise ValueError("plusieurs champs updated sont présents dans le front matter")
newline = "\r\n" if lines[0].endswith("\r\n") else "\n"
updated_line = f"updated = {timestamp}{newline}"
if updated_indexes:
lines[updated_indexes[0]] = updated_line
else:
try:
date_index = next(
index
for index in range(1, closing_index)
if DATE_RE.fullmatch(lines[index].rstrip("\r\n")) is not None
)
except StopIteration as exc:
raise ValueError("champ date absent du front matter") from exc
lines.insert(date_index + 1, updated_line)
try:
path.write_text("".join(lines), encoding="utf-8")
except OSError as exc:
raise ValueError(f"écriture impossible ({exc})") from exc
def main() -> int:
args = parse_args()
try:
path = resolve_content_path(args.fiche)
timestamp = current_timestamp()
update_front_matter(path, timestamp)
except (ValueError, RuntimeError) as exc:
print(f"Erreur : {exc}", file=sys.stderr)
return 1
print(f"Fiche mise à jour : {path.relative_to(ROOT_DIR)}")
print(f"updated = {timestamp}")
return 0
if __name__ == "__main__":
raise SystemExit(main())