Simplification du script d'horodatage et quelques corrections mineures
This commit is contained in:
+2
-2
@@ -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 lorsqu’il 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` l’active, remplace sa valeur ou insère `updated` après `date` lorsqu’il 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 l’unique ligne `updated`, qu’elle soit commentée ou active, par l’horodatage 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 l’horodatage déjà présent ou insère `updated` après `date` lorsqu’il 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
|
||||
|
||||
|
||||
@@ -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 l’horodatage existant ou insère `updated` après `date` lorsqu’il est absent.
|
||||
Le script remplace l’unique ligne `updated`, qu’elle 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
@@ -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
@@ -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 d’une 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
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<dt>Nature grammaticale</dt>
|
||||
<dd>{{ macros::taxonomy_links(terms=page.taxonomies.natures, kind="natures") }}</dd>
|
||||
|
||||
{# Tera n’accepte pas les tableaux imbriqués dans un littéral set.
|
||||
{# Tera 1 n’accepte 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") }}
|
||||
|
||||
Reference in New Issue
Block a user