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 ;
|
- `description` est un résumé court, rédigé manuellement lorsqu’il 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` 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` :
|
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 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
|
## 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
|
./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
|
## 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
@@ -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",
|
||||||
|
|||||||
+40
-92
@@ -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 d’une 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") == "+++"
|
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:
|
return timestamp
|
||||||
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:
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<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 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. #}
|
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") }}
|
||||||
|
|||||||
Reference in New Issue
Block a user