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
+1 -1
View File
@@ -1,4 +1,4 @@
"""Définitions communes au générateur et au validateur de contenus."""
"""Définitions communes aux outils de gestion des contenus."""
SECTION_ALIASES = {
"article": "articles",
+15 -5
View File
@@ -5,7 +5,7 @@ import re
import sys
import tomllib
import unicodedata
from datetime import date
from datetime import datetime
from pathlib import Path
from typing import Any
@@ -93,13 +93,22 @@ def require_string(
validator.error(path, f"{key} doit être une chaîne non vide")
def require_date(
def require_timestamp(
metadata: dict[str, Any],
key: str,
path: Path,
validator: Validator,
*,
optional: bool = False,
) -> None:
if not isinstance(metadata.get("date"), date):
validator.error(path, "date doit être une date TOML valide")
value = metadata.get(key)
if optional and value is None:
return
if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
validator.error(
path,
f"{key} doit être un horodatage TOML avec fuseau horaire",
)
def check_structure(
@@ -130,7 +139,8 @@ def check_structure(
require_string(metadata, "title", path, validator)
if section:
require_date(metadata, path, validator)
require_timestamp(metadata, "date", path, validator)
require_timestamp(metadata, "updated", path, validator, optional=True)
return section, False
+5 -5
View File
@@ -134,7 +134,7 @@ def build_vocabulary(title: str, timestamp: str, nature: str) -> str:
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DD
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
@@ -151,7 +151,7 @@ def build_sign(title: str, timestamp: str, mzl: str) -> str:
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DD
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
@@ -170,7 +170,7 @@ def build_page(section: str, title: str, timestamp: str) -> str:
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DD
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
@@ -185,7 +185,7 @@ banner_credit = ""
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DD
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
@@ -198,7 +198,7 @@ themes = []
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DD
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
+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())