init
This commit is contained in:
186
backend/montage.py
Normal file
186
backend/montage.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Buch aus den Kapiteldateien zusammensetzen: Guide-JSON, README, Gesamt-Markdown, PDF."""
|
||||
import html
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from . import config, gliederung, state
|
||||
|
||||
log = logging.getLogger("buchwerk")
|
||||
ABSCHNITT_RE = re.compile(r"^## (\d+\.\d+) (.+?)\s*$", re.M)
|
||||
|
||||
|
||||
def _woerter(text: str) -> int:
|
||||
return len(re.sub(r"```.*?```", "", text, flags=re.S).split())
|
||||
|
||||
|
||||
def _kapitel_lesen(v, k: dict) -> dict:
|
||||
p = v / "buch" / k["datei"]
|
||||
aus = {"id": f"{k['n']:02d}", "ord": k["n"], "titel": k["titel"], "teil": k["teil"], "datei": f"buch/{k['datei']}",
|
||||
"status": "fehlt", "intro": "", "abschnitte": [], "woerter": 0, "minuten": 0,
|
||||
"berichte": {"autor": state.bericht_lesen(v.name, f"kapitel-{k['n']:02d}"),
|
||||
"pruefer": state.bericht_lesen(v.name, f"pruefung-{k['n']:02d}")}}
|
||||
if not p.exists():
|
||||
return aus
|
||||
text = p.read_text()
|
||||
text = re.sub(r"^# .*\n", "", text, count=1)
|
||||
treffer = list(ABSCHNITT_RE.finditer(text))
|
||||
aus["intro"] = text[:treffer[0].start()].strip() if treffer else text.strip()
|
||||
for i, m in enumerate(treffer):
|
||||
ende = treffer[i + 1].start() if i + 1 < len(treffer) else len(text)
|
||||
inhalt = text[m.end():ende].strip()
|
||||
w = _woerter(inhalt)
|
||||
aus["abschnitte"].append({"id": f"{k['n']:02d}-{m.group(1).split('.')[1]}", "nr": m.group(1),
|
||||
"titel": m.group(2), "text": inhalt, "woerter": w,
|
||||
"minuten": round(w / config.LESE_WPM)})
|
||||
aus["woerter"] = _woerter(text)
|
||||
aus["minuten"] = round(aus["woerter"] / config.LESE_WPM)
|
||||
st = state.zustand(v.name)
|
||||
if state.stufe(v.name, "konsistenz")["status"] == "fertig":
|
||||
aus["status"] = "konsistent"
|
||||
elif aus["berichte"]["pruefer"]:
|
||||
aus["status"] = "geprueft"
|
||||
else:
|
||||
aus["status"] = "geschrieben"
|
||||
return aus
|
||||
|
||||
|
||||
def guide(name: str) -> dict:
|
||||
v = state.verzeichnis(name)
|
||||
t = state.thema(name) or {}
|
||||
d = state.deutung(name) or {}
|
||||
gp = v / "buch" / "GLIEDERUNG.md"
|
||||
g = gliederung.parsen(gp.read_text()) if gp.exists() else {"titel": "", "untertitel": "", "kapitel": []}
|
||||
kapitel = [_kapitel_lesen(v, k) for k in g["kapitel"]]
|
||||
vorwort = ""
|
||||
vp = v / "buch" / "00-vorwort.md"
|
||||
if vp.exists():
|
||||
vorwort = re.sub(r"^# .*\n", "", vp.read_text(), count=1).strip()
|
||||
qp = v / "quellen" / "QUELLEN.md"
|
||||
woerter = sum(k["woerter"] for k in kapitel)
|
||||
return {"titel": d.get("titel") or g["titel"] or t.get("titel") or name,
|
||||
"untertitel": d.get("untertitel") or g["untertitel"] or "",
|
||||
"status": state.zustand(name)["status"], "vorwort": vorwort, "kapitel": kapitel,
|
||||
"woerter": woerter, "minuten": round(woerter / config.LESE_WPM),
|
||||
"gliederung": gp.read_text() if gp.exists() else "",
|
||||
"quellen": qp.read_text() if qp.exists() else "",
|
||||
"pdf": (v / "buch" / "buch.pdf").exists()}
|
||||
|
||||
|
||||
def markdown(name: str) -> str:
|
||||
g = guide(name)
|
||||
v = state.verzeichnis(name)
|
||||
zeilen = [f"# {g['titel']}", ""]
|
||||
if g["untertitel"]:
|
||||
zeilen += [f"*{g['untertitel']}*", ""]
|
||||
if g["vorwort"]:
|
||||
zeilen += ["## Vorwort", "", g["vorwort"], ""]
|
||||
teil = None
|
||||
for k in g["kapitel"]:
|
||||
p = v / "buch" / k["datei"].split("/", 1)[1]
|
||||
if not p.exists():
|
||||
continue
|
||||
if k["teil"] and k["teil"] != teil:
|
||||
teil = k["teil"]
|
||||
zeilen += [f"# {teil}", ""]
|
||||
zeilen += [p.read_text().strip(), ""]
|
||||
return "\n".join(zeilen)
|
||||
|
||||
|
||||
def readme(name: str) -> str:
|
||||
g = guide(name)
|
||||
out = [f"# {g['titel']}", ""]
|
||||
if g["untertitel"]:
|
||||
out += [f"*{g['untertitel']}*", ""]
|
||||
out += ["PDF: [buch.pdf](buch.pdf) · Gesamttext: [buch.md](buch.md)", "", "## Inhalt", "", "[Vorwort](00-vorwort.md)", ""]
|
||||
teil = None
|
||||
for k in g["kapitel"]:
|
||||
if k["teil"] and k["teil"] != teil:
|
||||
teil = k["teil"]
|
||||
out += [f"### {teil}", ""]
|
||||
d = k["datei"].split("/", 1)[1]
|
||||
out += [f"**[Kapitel {k['ord']} – {k['titel']}]({d})** ",
|
||||
" · ".join(f"{a['nr']} {a['titel']}" for a in k["abschnitte"]), ""]
|
||||
out += [f"*{len(g['kapitel'])} Kapitel, rund {round(g['woerter'], -3):,} Wörter.*".replace(",", "."), ""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---- PDF ----------------------------------------------------------------------
|
||||
|
||||
CSS = """
|
||||
@page{size:A5;margin:17mm 14mm 19mm 14mm;@bottom-center{content:counter(page);font:9pt "DejaVu Serif"}}
|
||||
@page:first{@bottom-center{content:none}}
|
||||
body{font-family:"DejaVu Serif",serif;font-size:9.3pt;line-height:1.42;text-align:left;hyphens:auto}
|
||||
h1.chap{font-size:16pt;margin:0 0 9mm;page-break-before:always}
|
||||
h2{font-size:11.8pt;margin:7mm 0 2mm;page-break-after:avoid}
|
||||
h3{font-size:10pt;margin:4mm 0 1mm;page-break-after:avoid}
|
||||
p{margin:0 0 2.2mm;orphans:2;widows:2}
|
||||
code{font-family:"DejaVu Sans Mono",monospace;font-size:7.9pt;hyphens:none}
|
||||
.codehilite,pre{background:#f5f5f2!important;border:0.4pt solid #ccc;padding:2mm;margin:2mm 0 3mm;border-radius:1mm}
|
||||
pre{white-space:pre-wrap;word-wrap:break-word;font-size:7.3pt;line-height:1.3;margin:0;border:none;padding:0}
|
||||
table{border-collapse:collapse;font-size:8pt;margin:3mm 0;width:100%}
|
||||
td,th{border:0.4pt solid #888;padding:1.2mm;vertical-align:top;text-align:left}
|
||||
ul,ol{padding-left:5mm} li{margin-bottom:1.2mm}
|
||||
.title{page-break-after:always;text-align:center;padding-top:50mm}
|
||||
.title h1{font-size:28pt;margin:0}.title p{font-size:12pt}
|
||||
.part{page-break-before:always;page-break-after:always;text-align:center;padding-top:60mm}
|
||||
.parttitle{font-size:19pt}
|
||||
.toc{page-break-before:always}.toc ul{list-style:none;padding:0}
|
||||
.tpart{font-weight:bold;margin-top:4mm}.tch a{color:black;text-decoration:none}
|
||||
.tch a::after{content:leader(".") target-counter(attr(href),page)}
|
||||
a{color:black}
|
||||
"""
|
||||
|
||||
|
||||
def pdf(name: str) -> bool:
|
||||
exe = shutil.which("weasyprint")
|
||||
if not exe:
|
||||
log.warning("weasyprint fehlt – kein PDF")
|
||||
return False
|
||||
import markdown as md_mod
|
||||
g = guide(name)
|
||||
v = state.verzeichnis(name)
|
||||
md = md_mod.Markdown(extensions=["tables", "sane_lists", "fenced_code", "codehilite"],
|
||||
extension_configs={"codehilite": {"noclasses": True, "pygments_style": "friendly", "guess_lang": False}})
|
||||
body, toc, teil = [], [], None
|
||||
if g["vorwort"]:
|
||||
body.append(f'<section class="chapter"><h1 id="k0" class="chap">Vorwort</h1>{md.convert(g["vorwort"])}</section>')
|
||||
toc.append('<li class="tch"><a href="#k0">Vorwort</a></li>')
|
||||
md.reset()
|
||||
for k in g["kapitel"]:
|
||||
p = v / "buch" / k["datei"].split("/", 1)[1]
|
||||
if not p.exists():
|
||||
continue
|
||||
if k["teil"] and k["teil"] != teil:
|
||||
teil = k["teil"]
|
||||
tt = html.escape(teil).replace(" – ", "<br>", 1)
|
||||
body.append(f'<section class="part"><h1 class="parttitle">{tt}</h1></section>')
|
||||
toc.append(f'<li class="tpart">{html.escape(teil)}</li>')
|
||||
aid = f"k{k['ord']}"
|
||||
t = f"Kapitel {k['ord']} – {k['titel']}"
|
||||
toc.append(f'<li class="tch"><a href="#{aid}">{html.escape(t)}</a></li>')
|
||||
h = md.convert(p.read_text()).replace("<h1>", f'<h1 id="{aid}" class="chap">', 1)
|
||||
h = h.replace("border: 1px solid #FF0000", "border: none")
|
||||
body.append(f'<section class="chapter">{h}</section>')
|
||||
md.reset()
|
||||
doc = (f'<!doctype html><html lang="de"><head><meta charset="utf-8"><title>{html.escape(g["titel"])}</title>'
|
||||
f'<style>{CSS}</style></head><body><div class="title"><h1>{html.escape(g["titel"])}</h1>'
|
||||
f'<p>{html.escape(g["untertitel"])}</p></div><div class="toc"><h1 style="font-size:16pt">Inhalt</h1>'
|
||||
f'<ul>{"".join(toc)}</ul></div>{"".join(body)}</body></html>')
|
||||
hp, pp = v / "arbeit" / "buch.html", v / "buch" / "buch.pdf"
|
||||
hp.parent.mkdir(exist_ok=True)
|
||||
hp.write_text(doc)
|
||||
r = subprocess.run([exe, str(hp), str(pp)], capture_output=True, text=True, timeout=900)
|
||||
if r.returncode != 0:
|
||||
log.error("weasyprint: %s", r.stderr[-500:])
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def bauen(name: str) -> bool:
|
||||
"""README, Gesamt-Markdown, PDF. Läuft in einem Thread – keine Zustandsänderungen hier."""
|
||||
v = state.verzeichnis(name)
|
||||
(v / "buch" / "README.md").write_text(readme(name))
|
||||
(v / "buch" / "buch.md").write_text(markdown(name))
|
||||
return pdf(name)
|
||||
Reference in New Issue
Block a user