DOCS/tools/build_kanban.py
2026-09-03 14:00:51 +02:00

125 lines
5.1 KiB
Python

"""Regenere DOCS/kanban.html depuis DOCS/kanban/.
python3 DOCS/tools/build_kanban.py
Les compteurs de colonne, le bandeau de chiffres cles et le « Fait recemment »
sont calcules a partir du nombre de fichiers : ils ne peuvent plus se
desynchroniser du contenu reel.
Ajouter un chantier : creer un .md dans kanban/cards/<colonne>/
Deplacer un chantier : deplacer le fichier vers un autre dossier de colonne
Clore un chantier : deplacer le fichier vers kanban/done/ (ne garder que « title »)
L'ordre d'affichage suit le prefixe numerique du nom de fichier (pas de 10).
"""
import io
import json
import os
import sys
DOCS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
KANBAN = os.path.join(DOCS, "kanban")
CARDS_DIR = os.path.join(KANBAN, "cards")
DONE_DIR = os.path.join(KANBAN, "done")
TPL = os.path.join(DOCS, "kanban-template.html")
OUT = os.path.join(DOCS, "kanban.html")
def read_item(path):
raw = io.open(path, encoding="utf-8", newline="").read().replace("\r\n", "\n")
if not raw.startswith("---\n") or "\n---\n" not in raw:
sys.exit("%s : frontmatter manquant ou mal ferme" % os.path.basename(path))
front, body = raw[4:].split("\n---\n", 1)
item = {"body": body.strip(), "tags": [], "flag": None, "horizon": None}
for line in front.strip().split("\n"):
key, _, value = line.partition(":")
key, value = key.strip(), value.strip()
if key == "tags":
item["tags"] = [t.strip() for t in value.split(",") if t.strip()]
elif key == "flag":
level, _, text = value.partition("|")
item["flag"] = {"level": level.strip(), "text": text.strip()}
else:
item[key] = value
if not item.get("title"):
sys.exit("%s : champ « title » manquant" % os.path.basename(path))
return item
def md_files(folder):
return [os.path.join(folder, f) for f in sorted(os.listdir(folder)) if f.endswith(".md")]
def render_card(card, path):
for required in ("area", "src"):
if not card.get(required):
sys.exit("%s : champ « %s » manquant" % (os.path.basename(path), required))
attrs = ' data-area="%s"' % card["area"]
if card["horizon"]:
attrs += ' data-horizon="%s"' % card["horizon"]
meta = "".join('<span class="tag">%s</span>' % t for t in card["tags"])
if card["flag"]:
meta += '<span class="flag f-%s">%s</span>' % (card["flag"]["level"], card["flag"]["text"])
body = "\n ".join(card["body"].split("\n"))
return (
' <article class="card"%s>\n'
' <div class="card-meta">%s</div>\n'
" <h3>%s</h3>\n"
" %s\n"
' <span class="src">%s</span>\n'
" </article>"
) % (attrs, meta, card["title"], body, card["src"])
board = json.load(io.open(TPL.replace("kanban-template.html", "kanban/board.json"), encoding="utf-8"))
sections, counts = [], []
for col in board["columns"]:
paths = md_files(os.path.join(CARDS_DIR, col["dir"]))
cards = [render_card(read_item(p), p) for p in paths]
counts.append((col, len(cards)))
sections.append(
" <!-- %s -->\n"
' <section class="col" style="--stripe: var(--%s)">\n'
' <div class="col-head"><h2>%s</h2><span class="count">%d</span></div>\n'
' <div class="stack">\n\n%s\n\n </div>\n'
" </section>" % (col["comment"], col["stripe"], col["title"], len(cards), "\n\n".join(cards))
)
done_items = [read_item(p) for p in md_files(DONE_DIR)]
done_html = "\n\n".join(
' <div class="done-item">\n <strong>%s</strong>\n %s\n </div>'
% (it["title"], "\n ".join(it["body"].split("\n")))
for it in done_items
)
done = board["done"]
done_section = (
'<section class="done">\n <h2>%s</h2>\n <p>%s</p>\n <div class="done-grid">\n\n%s\n\n </div>\n </section>'
% (done["heading"], done["intro"], done_html)
)
stats = [
' <div class="stat"><span class="n%s">%d</span><span class="k">%s</span></div>'
% ((" " + col["statClass"]) if col["statClass"] else "", n, col["title"])
for col, n in counts
]
stats.append(
' <div class="stat"><span class="n %s">%d</span><span class="k">%s</span></div>'
% (done["statClass"], len(done_items), done["statLabel"])
)
html = io.open(TPL, encoding="utf-8", newline="").read().replace("\r\n", "\n")
html = html.replace("{{INTRO}}", "\n".join(" <p>%s</p>" % p for p in board["intro"]))
html = html.replace("{{STAMP}}", "Mise à jour · %s<br>\n %s" % (board["updated"], board["stampNote"]))
html = html.replace(
"{{SUMMARY}}", ' <section class="summary" aria-label="Chiffres clés">\n%s\n </section>' % "\n".join(stats)
)
html = html.replace("{{BOARD}}", '<div class="board">\n\n%s\n\n </div>' % "\n\n".join(sections))
html = html.replace("{{DONE}}", done_section)
io.open(OUT, "w", encoding="utf-8", newline="\n").write(html)
print("kanban.html : %d o" % os.path.getsize(OUT))
print("%d chantiers ouverts + %d clos" % (sum(n for _, n in counts), len(done_items)))
for col, n in counts:
print(" %-14s %2d" % (col["title"], n))