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:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Horodate la dernière modification substantielle d’une 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 d’une 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())
|
||||
Reference in New Issue
Block a user