"""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) KERN_RE = re.compile(r"^## (?:Kern|\d+\.\d+ Merken)[ \t]*\n(.*?)(?=^## |\Z)", re.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 kern(name: str) -> str: """Alle Merken-Abschnitte (bzw. ältere `## Kern`-Blöcke) der Kapitel in einer Datei – zum Auffrischen eines gelesenen Buchs.""" g = guide(name); v = state.verzeichnis(name) out = [f"# Kernerkenntnisse – {g['titel']}", ""] for k in g["kapitel"]: p = v / "buch" / k["datei"].split("/", 1)[1] m = KERN_RE.search(p.read_text()) if p.exists() else None if m: out += [f"## Kapitel {k['ord']} – {k['titel']}", "", m.group(1).strip(), ""] return "\n".join(out) 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) · Kernerkenntnisse: [KERN.md](KERN.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} pre.diagramm{white-space:pre;font-size:6.8pt;line-height:1.15;page-break-inside:avoid;width:max-content;max-width:100%;margin:2mm auto 3mm;padding:2mm;border:0.4pt solid #ccc;border-radius:1mm;background:#f5f5f2} 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} """ DIAGRAMM_RE = re.compile(r"^```diagramm[ \t]*\n(.*?)^```[ \t]*$", re.S | re.M) def _vorbereiten(text: str) -> str: """Textdiagramme als unformatierte
 durchreichen; gdshader mit dem GLSL-Lexer hervorheben."""
    text = DIAGRAMM_RE.sub(lambda m: '
' + html.escape(m.group(1).rstrip("\n")) + "
", text) return re.sub(r"^```gdshader[ \t]*$", "```glsl", text, flags=re.M) 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'

Vorwort

{md.convert(g["vorwort"])}
') toc.append('
  • Vorwort
  • ') 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(" – ", "
    ", 1) body.append(f'

    {tt}

    ') toc.append(f'
  • {html.escape(teil)}
  • ') aid = f"k{k['ord']}" t = f"Kapitel {k['ord']} – {k['titel']}" toc.append(f'
  • {html.escape(t)}
  • ') h = md.convert(_vorbereiten(p.read_text())).replace("

    ", f'

    ', 1) h = re.sub(r"border: ?1px solid #F(?:F0000|00)", "", h) # Pygments markiert @-Annotationen in GDScript als Fehler h = h.replace("border: 1px solid #FF0000", "border: none") body.append(f'
    {h}
    ') md.reset() doc = (f'{html.escape(g["titel"])}' f'

    {html.escape(g["titel"])}

    ' f'

    {html.escape(g["untertitel"])}

    Inhalt

    ' f'
      {"".join(toc)}
    {"".join(body)}') 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)) (v / "buch" / "KERN.md").write_text(kern(name)) return pdf(name)