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

134 lines
5.6 KiB
Python

"""Extraction unique : kanban.html -> DOCS/kanban/ + kanban-template.html.
Une carte = un fichier, un « fait recemment » = un fichier. Ensuite kanban.html
est regenere par build_kanban.py et ne s'edite plus a la main.
"""
import io
import json
import os
import re
import unicodedata
DOCS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC = os.path.join(DOCS, "kanban.html")
KANBAN = os.path.join(DOCS, "kanban")
CARDS_DIR = os.path.join(KANBAN, "cards")
DONE_DIR = os.path.join(KANBAN, "done")
BOARD_OUT = os.path.join(KANBAN, "board.json")
TPL_OUT = os.path.join(DOCS, "kanban-template.html")
def write(path, text):
io.open(path, "w", encoding="utf-8", newline="\n").write(text)
def slug(text, maxlen=45):
text = re.sub(r"<[^>]+>", "", text)
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
text = re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower()
return text[:maxlen].rstrip("-") or "sans-titre"
html = io.open(SRC, encoding="utf-8", newline="").read()
masthead = re.search(r'<header class="masthead">(.*?)</header>', html, re.S).group(1)
intro = [p.strip() for p in re.findall(r"<p>(.*?)</p>", masthead, re.S)]
stamp = re.search(r'<div class="stamp">(.*?)</div>', masthead, re.S).group(1).strip()
summary_block = re.search(r'<section class="summary".*?</section>', html, re.S).group(0)
stats = re.findall(r'<span class="n([^"]*)">(\d+)</span><span class="k">([^<]*)</span>', summary_block)
stat_class = {k: cls.strip() for cls, n, k in stats}
done_start = html.index('<section class="done">')
board_start = html.index('<div class="board">')
board_end = html.rindex("</section>", 0, done_start)
board_end = html.index("</div>", board_end) + len("</div>")
board = html[board_start:board_end]
columns = []
n_cards = 0
for pos, col in enumerate(
re.finditer(
r'<!--\s*([^>]*?)\s*-->\s*<section class="col" style="--stripe: var\(--([\w-]+)\)">\s*'
r'<div class="col-head"><h2>([^<]*)</h2><span class="count">\d+</span></div>\s*'
r'<div class="stack">(.*?)</div>\s*</section>',
board,
re.S,
),
start=1,
):
comment, stripe, title, stack = col.groups()
dirname = "%d-%s" % (pos, slug(title))
os.makedirs(os.path.join(CARDS_DIR, dirname), exist_ok=True)
for rank, card in enumerate(re.finditer(r'<article class="card"([^>]*)>(.*?)</article>', stack, re.S), start=1):
attrs, inner = card.groups()
horizon = re.search(r'data-horizon="([^"]*)"', attrs)
meta = re.search(r'<div class="card-meta">(.*?)</div>', inner, re.S).group(1)
flag = re.search(r'<span class="flag f-(\w+)">(.*?)</span>', meta, re.S)
card_title = re.search(r"<h3>(.*?)</h3>", inner, re.S).group(1).strip()
front = ["title: " + card_title, "area: " + re.search(r'data-area="([^"]*)"', attrs).group(1)]
if horizon:
front.append("horizon: " + horizon.group(1))
front.append("tags: " + ", ".join(re.findall(r'<span class="tag">(.*?)</span>', meta, re.S)))
if flag:
front.append("flag: %s | %s" % (flag.group(1), flag.group(2).strip()))
front.append("src: " + re.search(r'<span class="src">(.*?)</span>', inner, re.S).group(1).strip())
body = "\n".join("<p>%s</p>" % p.strip() for p in re.findall(r"<p>(.*?)</p>", inner, re.S))
write(
os.path.join(CARDS_DIR, dirname, "%03d-%s.md" % (rank * 10, slug(card_title))),
"---\n" + "\n".join(front) + "\n---\n" + body + "\n",
)
n_cards += 1
columns.append(
{"dir": dirname, "title": title, "stripe": stripe, "statClass": stat_class.get(title, ""), "comment": comment}
)
done_section = html[done_start : html.index("</section>", html.index('<div class="done-grid">', done_start))]
os.makedirs(DONE_DIR, exist_ok=True)
n_done = 0
for rank, item in enumerate(re.finditer(r'<div class="done-item">\s*(.*?)\s*</div>', done_section, re.S), start=1):
inner = item.group(1)
title = re.search(r"<strong>(.*?)</strong>", inner, re.S).group(1).strip()
body = inner[inner.index("</strong>") + len("</strong>") :].strip()
write(
os.path.join(DONE_DIR, "%03d-%s.md" % (rank * 10, slug(title))),
"---\ntitle: " + title + "\n---\n" + body + "\n",
)
n_done += 1
board_meta = {
"updated": re.search(r"Mise à jour · ([\d-]+)", stamp).group(1),
"stampNote": stamp.split("<br>")[-1].strip(),
"intro": intro,
"columns": columns,
"done": {
"heading": re.search(r"<h2>(.*?)</h2>", done_section, re.S).group(1).strip(),
"statLabel": next((k for k in stat_class if "Fait" in k), "Fait récemment"),
"statClass": next((v for k, v in stat_class.items() if "Fait" in k), "n-good"),
"intro": re.search(r"<p>(.*?)</p>", done_section, re.S).group(1).strip(),
},
}
write(BOARD_OUT, json.dumps(board_meta, ensure_ascii=False, indent=2) + "\n")
template = (
html[: html.index('<header class="masthead">')]
+ '<header class="masthead">\n <div>\n '
+ re.search(r"<h1>.*?</h1>", masthead, re.S).group(0)
+ "\n{{INTRO}}\n"
+ ' <div class="stamp">\n {{STAMP}}\n </div>\n </header>\n\n'
+ "{{SUMMARY}}\n\n"
+ html[html.index('<div class="filters"') : board_start]
+ "{{BOARD}}\n\n {{DONE}}\n"
+ html[html.index("</section>", html.index('<div class="done-grid">')) + len("</section>") :]
)
write(TPL_OUT, template)
print("cartes : %d fait-recemment : %d" % (n_cards, n_done))
for c in columns:
print(" %-14s %2d" % (c["title"], len(os.listdir(os.path.join(CARDS_DIR, c["dir"])))))
print("board.json %d o template %d o" % (os.path.getsize(BOARD_OUT), os.path.getsize(TPL_OUT)))