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 ; - `description` est un résumé court, rédigé manuellement lorsquil est utile ;
- `draft` détermine si la page est publiée. - `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` : 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 ./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 ## 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 ./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 ## 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/new-content.py` crée les brouillons ;
- `scripts/update-content.py` horodate les modifications substantielles ; - `scripts/update-content.py` horodate les modifications substantielles ;
- `scripts/check-content.py` valide les contenus ; - `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. 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 = { SECTION_ALIASES = {
"article": "articles", "article": "articles",
+38 -90
View File
@@ -4,134 +4,82 @@
import argparse import argparse
import re import re
import sys import sys
import tomllib
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError 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 FRONT_MATTER_RE = re.compile(
CONTENT_DIR = ROOT_DIR / "src" / "content" r"\A\+\+\+\r?\n(?P<body>.*?)(?:\r?\n\+\+\+(?=\r?\n|\Z))",
TIMEZONE_NAME = "Europe/Paris" re.DOTALL,
)
UPDATED_RE = re.compile(r"\s*(?:#\s*)?updated\s*=.*") UPDATED_RE = re.compile(r"(?m)^(?:#\s*)?updated\s*=.*?(?=\r?$)")
DATE_RE = re.compile(r"\s*date\s*=.*")
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description=__doc__)
description="Horodater la dernière modification substantielle dune fiche."
)
parser.add_argument( parser.add_argument(
"fiche", "fiche",
help=( help="chemin depuis la racine du dépôt ou depuis src/content/",
"chemin depuis la racine du dépôt ou depuis src/content/ "
"(par exemple vocabulaire/bītum.md)"
),
) )
return parser.parse_args() return parser.parse_args()
def current_timestamp() -> str: def resolve_path(value: str) -> Path:
try: path = Path(value).expanduser()
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")
if not path.is_absolute():
def resolve_content_path(value: str) -> Path: path = ROOT / path if path.parts[:2] == ("src", "content") else CONTENT / 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() 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(): if not path.is_file():
raise ValueError(f"fichier introuvable : {path}") raise ValueError(f"fichier introuvable : {path}")
if path.suffix != ".md" or path.name == "_index.md": if path.suffix != ".md" or path.name == "_index.md":
raise ValueError("le chemin doit désigner une fiche Markdown, pas un index") 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 return path
def update_front_matter(path: Path, timestamp: str) -> None: def update_timestamp(path: Path) -> str:
try: text = path.read_bytes().decode("utf-8")
text = path.read_text(encoding="utf-8") front_matter = FRONT_MATTER_RE.match(text)
except (OSError, UnicodeDecodeError) as exc:
raise ValueError(f"lecture impossible ({exc})") from exc
lines = text.splitlines(keepends=True) if front_matter is None:
if not lines or lines[0].rstrip("\r\n") != "+++": raise ValueError("front matter TOML délimité par +++ absent")
raise ValueError("front matter TOML initial absent")
try: matches = list(UPDATED_RE.finditer(front_matter.group("body")))
closing_index = next( if len(matches) != 1:
index raise ValueError("la fiche doit contenir exactement une ligne updated")
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: timestamp = datetime.now(ZoneInfo("Europe/Paris")).isoformat(timespec="seconds")
tomllib.loads("".join(lines[1:closing_index]))
except tomllib.TOMLDecodeError as exc:
raise ValueError(f"front matter TOML invalide ({exc})") from exc
updated_indexes = [ match = matches[0]
index offset = front_matter.start("body")
for index in range(1, closing_index) start = offset + match.start()
if UPDATED_RE.fullmatch(lines[index].rstrip("\r\n")) is not None end = offset + match.end()
]
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" path.write_bytes(
updated_line = f"updated = {timestamp}{newline}" (text[:start] + f"updated = {timestamp}" + text[end:]).encode("utf-8")
)
if updated_indexes: return timestamp
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: def main() -> int:
args = parse_args() args = parse_args()
try: try:
path = resolve_content_path(args.fiche) path = resolve_path(args.fiche)
timestamp = current_timestamp() timestamp = update_timestamp(path)
update_front_matter(path, timestamp) except (OSError, UnicodeError, ValueError, ZoneInfoNotFoundError) as exc:
except (ValueError, RuntimeError) as exc:
print(f"Erreur : {exc}", file=sys.stderr) print(f"Erreur : {exc}", file=sys.stderr)
return 1 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}") print(f"updated = {timestamp}")
return 0 return 0
+2 -2
View File
@@ -30,8 +30,8 @@
<dt>Nature grammaticale</dt> <dt>Nature grammaticale</dt>
<dd>{{ macros::taxonomy_links(terms=page.taxonomies.natures, kind="natures") }}</dd> <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. #} 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_field(extra=page.extra, key="meaning", label="Sens") }}
{{ macros::metadata_list(extra=page.extra, key="logograms", label="Logogrammes", lang="sux-Latn") }} {{ macros::metadata_list(extra=page.extra, key="logograms", label="Logogrammes", lang="sux-Latn") }}
{{ macros::metadata_field(extra=page.extra, key="gender", label="Genre") }} {{ macros::metadata_field(extra=page.extra, key="gender", label="Genre") }}