Simplification du script d'horodatage et quelques corrections mineures

This commit is contained in:
julien
2026-08-05 18:29:51 +02:00
parent 65642a5983
commit 81616e11e3
6 changed files with 46 additions and 98 deletions
+2 -2
View File
@@ -94,7 +94,7 @@ Les tables propres à chaque type de contenu viennent ensuite dans le même fron
- `description` est un résumé court, rédigé manuellement lorsquil est utile ;
- `draft` détermine si la page est publiée.
`date` est un horodatage TOML écrit sans guillemets dans le fuseau `Europe/Paris`. La ligne commentée `# updated = YYYY-MM-DDTHH:MM:SS+HH:MM` est un repère inactif. Après une modification substantielle, `update-content.py` lactive, remplace sa valeur ou insère `updated` après `date` lorsquil est absent.
`date` est un horodatage TOML écrit sans guillemets dans le fuseau `Europe/Paris`. La ligne commentée `# updated = YYYY-MM-DDTHH:MM:SS+HH:MM` est un repère inactif. Après une modification substantielle, `update-content.py` remplace lunique ligne `updated`, quelle soit commentée ou active, par lhorodatage courant. La fiche doit toujours conserver exactement une telle ligne.
Le résultat utilise le même format que `date` :
@@ -180,7 +180,7 @@ Le chemin complet se sélectionne rapidement avec la touche Tab du shell. Une fo
./scripts/update-content.py vocabulaire/bītum.md
```
Le script refuse les index, les fichiers extérieurs à `src/content/` et les sections inconnues. Il active le repère commenté, remplace lhorodatage déjà présent ou insère `updated` après `date` lorsquil est absent, sans modifier le reste du fichier.
Le script refuse les index et les fichiers extérieurs à `src/content/`. Il exige exactement une ligne `updated`, commentée ou active, puis la remplace sans modifier le reste du fichier.
## Taxonomies
+2 -2
View File
@@ -117,7 +117,7 @@ Ce chemin se complète directement avec la touche Tab du shell. La forme abrég
./scripts/update-content.py vocabulaire/bītum.md
```
Le script active le repère commenté, remplace lhorodatage existant ou insère `updated` après `date` lorsquil est absent.
Le script remplace lunique ligne `updated`, quelle soit commentée ou active. La fiche doit donc toujours conserver cette ligne.
## Publier un contenu
@@ -180,7 +180,7 @@ Une erreur renvoie un code de sortie non nul et bloque également la constructio
- `scripts/new-content.py` crée les brouillons ;
- `scripts/update-content.py` horodate les modifications substantielles ;
- `scripts/check-content.py` valide les contenus ;
- `scripts/_schema.py` contient les définitions communes aux outils de gestion des contenus.
- `scripts/_schema.py` contient les définitions communes au générateur et au validateur.
Les chemins et les taxonomies utilisent la stratégie de slugification `safe` de Zola. Les diacritiques sont donc conservés : des lectures comme `tim` et `ṭim` restent distinctes.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Définitions communes aux outils de gestion des contenus."""
"""Définitions communes au générateur et au validateur de contenus."""
SECTION_ALIASES = {
"article": "articles",
+40 -92
View File
@@ -4,134 +4,82 @@
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 = Path(__file__).resolve().parent.parent
CONTENT = (ROOT / "src" / "content").resolve()
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*=.*")
FRONT_MATTER_RE = re.compile(
r"\A\+\+\+\r?\n(?P<body>.*?)(?:\r?\n\+\+\+(?=\r?\n|\Z))",
re.DOTALL,
)
UPDATED_RE = re.compile(r"(?m)^(?:#\s*)?updated\s*=.*?(?=\r?$)")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Horodater la dernière modification substantielle dune fiche."
)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"fiche",
help=(
"chemin depuis la racine du dépôt ou depuis src/content/ "
"(par exemple vocabulaire/bītum.md)"
),
help="chemin depuis la racine du dépôt ou depuis src/content/",
)
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_path(value: str) -> Path:
path = Path(value).expanduser()
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
if not path.is_absolute():
path = ROOT / path if path.parts[:2] == ("src", "content") else CONTENT / path
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_relative_to(CONTENT):
raise ValueError("la fiche doit se trouver dans src/content/")
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
def update_timestamp(path: Path) -> str:
text = path.read_bytes().decode("utf-8")
front_matter = FRONT_MATTER_RE.match(text)
lines = text.splitlines(keepends=True)
if not lines or lines[0].rstrip("\r\n") != "+++":
raise ValueError("front matter TOML initial absent")
if front_matter is None:
raise ValueError("front matter TOML délimité par +++ absent")
try:
closing_index = next(
index
for index, line in enumerate(lines[1:], start=1)
if line.rstrip("\r\n") == "+++"
matches = list(UPDATED_RE.finditer(front_matter.group("body")))
if len(matches) != 1:
raise ValueError("la fiche doit contenir exactement une ligne updated")
timestamp = datetime.now(ZoneInfo("Europe/Paris")).isoformat(timespec="seconds")
match = matches[0]
offset = front_matter.start("body")
start = offset + match.start()
end = offset + match.end()
path.write_bytes(
(text[:start] + f"updated = {timestamp}" + text[end:]).encode("utf-8")
)
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
return timestamp
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:
path = resolve_path(args.fiche)
timestamp = update_timestamp(path)
except (OSError, UnicodeError, ValueError, ZoneInfoNotFoundError) as exc:
print(f"Erreur : {exc}", file=sys.stderr)
return 1
print(f"Fiche mise à jour : {path.relative_to(ROOT_DIR)}")
print(f"Fiche mise à jour : {path.relative_to(ROOT)}")
print(f"updated = {timestamp}")
return 0
+1 -1
View File
@@ -30,7 +30,7 @@
<dt>Nature grammaticale</dt>
<dd>{{ macros::taxonomy_links(terms=page.taxonomies.natures, kind="natures") }}</dd>
{# Tera naccepte pas les tableaux imbriqués dans un littéral set.
{# Tera 1 naccepte pas les tableaux imbriqués dans un littéral set.
Les champs sont donc déclarés explicitement et leur balisage est factorisé par macro. #}
{{ macros::metadata_field(extra=page.extra, key="meaning", label="Sens") }}
{{ macros::metadata_list(extra=page.extra, key="logograms", label="Logogrammes", lang="sux-Latn") }}