"""Builders for README, metadata.yaml, manifest.json, report and diagnostics files.""" from __future__ import annotations import json import posixpath import shlex from collections import Counter from pathlib import Path from typing import Any from . import TOOL_NAME, __version__ from .diagnostics import Diagnostics from .html_converter import md_escape from .models import BookMetadata, Chapter, NavEntry from .references import LinkResolver from .utils import rel_link, to_yaml, url_path CALLOUT_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "callouts.lua" PRINT_CSS = Path(__file__).resolve().parent.parent / "pandoc" / "print.css" BOOK_FILTER = Path(__file__).resolve().parent.parent / "pandoc" / "book.lua" SPECIAL_LABELS = { "glossary": "Glossar", "bibliography": "Literatur / Quellen", "index": "Sachregister", "index-persons": "Personenregister", "endnotes": "Anmerkungen", "appendix": "Anhang", } def chapter_frontmatter(ch: Chapter, md: BookMetadata, source_epub: str, opf_dir: str, warnings: int) -> str: def rel(p: str) -> str: return p[len(opf_dir) + 1 :] if opf_dir and p.startswith(opf_dir + "/") else p data: dict[str, Any] = { "book_title": md.title, "book_author": md.authors, "language": md.language or None, "chapter_number": ch.number, "chapter_title": ch.title, "source_epub": source_epub, "source_href": rel(ch.docs[0]) if ch.docs else None, } if len(ch.docs) > 1: data["source_hrefs"] = [rel(d) for d in ch.docs] data["epub_type"] = ch.types data["conversion_status"] = "converted-with-warnings" if warnings else "converted" return "---\n" + to_yaml(data) + "---\n\n" def metadata_yaml(md: BookMetadata, source_name: str, sha256: str, epub_version: str, cover: str, options: dict[str, Any], generated_at: str) -> str: data: dict[str, Any] = {} def put(key: str, value: Any) -> None: if value not in ("", None, []): data[key] = value put("title", md.title) put("subtitle", md.subtitle) put("authors", md.authors) put("contributors", md.contributors) put("language", md.language) put("publisher", md.publisher) put("published", md.published) put("identifier", md.identifier) put("isbn", md.isbn) data["source_file"] = source_name data["source_sha256"] = sha256 put("epub_version", epub_version) put("description", md.description) put("rights", md.rights) put("subjects", md.subjects) put("cover_image", cover) data["conversion"] = {"tool": TOOL_NAME, "version": __version__, "generated_at": generated_at, "options": options} return to_yaml(data) def _nav_lines(entries: list[NavEntry], resolver: LinkResolver, depth: int = 0) -> list[str]: lines = [] for e in entries: label = md_escape(e.label or "(ohne Titel)") url = None if e.path: url, _ = resolver.resolve("README.md", e.path, ("#" + e.fragment) if e.fragment else "") lines.append(" " * depth + (f"- [{label}]({url})" if url else f"- {label}")) lines.extend(_nav_lines(e.children, resolver, depth + 1)) return lines def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None, nav: list[NavEntry], resolver: LinkResolver, stats: dict[str, int], cover: str, epub_version: str, source_name: str, command: list[str], output_dir: str = "OUTPUT_VERZEICHNIS", callouts: int = 0) -> str: out = [f"# {md_escape(md.title or source_name)}"] if md.subtitle: out.append(f"*{md_escape(md.subtitle)}*") if md.authors: out.append("**" + md_escape(", ".join(md.authors)) + "**") if cover: out.append(f"![Cover]({url_path(cover)})") rows = [("Sprache", md.language), ("Verlag", md.publisher), ("Veröffentlicht", md.published), ("Identifier", md.identifier), ("ISBN", md.isbn), ("EPUB-Version", epub_version), ("Quelldatei", source_name)] table = ["| Feld | Wert |", "| --- | --- |"] + [ f"| {k} | {md_escape(v, table=True)} |" for k, v in rows if v ] out.append("\n".join(table)) out.append("## Inhaltsverzeichnis") lines = [f"{c.number}. [{md_escape(c.title)}]({url_path(c.filename)})" + (" *(Zusatzseite, nicht in linearer Lesereihenfolge)*" if "auxiliary" in c.types else "") for c in chapters] if endnotes is not None: lines.append(f"{len(chapters) + 1}. [{md_escape(endnotes.title)}]({url_path(endnotes.filename)})") out.append("\n".join(lines) if lines else "*(keine Kapitel)*") specials = [(c, t) for c in chapters for t in c.types if t in SPECIAL_LABELS] if specials or endnotes is not None: out.append("## Besondere Abschnitte") sl = [f"- {SPECIAL_LABELS[t]}: [{md_escape(c.title)}]({url_path(c.filename)})" for c, t in specials] if endnotes is not None: sl.append(f"- Anmerkungen (gesammelt): [{md_escape(endnotes.title)}]({url_path(endnotes.filename)})") out.append("\n".join(sl)) if nav: out.append("## Original-Inhaltsverzeichnis (NAV/NCX)") out.append("\n".join(_nav_lines(nav, resolver))) out.append("## Statistik") labels = [("chapters", "Kapitel"), ("images", "Bilder"), ("svgs", "SVG-Grafiken"), ("media_files", "Extrahierte Mediendateien"), ("code_blocks", "Codeblöcke"), ("formulas", "Erkannte Formeln"), ("formulas_as_image", "Formeln als Bild/SVG belassen"), ("formulas_as_mathml", "Formeln als MathML belassen"), ("footnotes", "Fußnoten/Anmerkungen"), ("warnings", "Warnungen")] out.append("\n".join(["| Kennzahl | Anzahl |", "| --- | ---: |"] + [f"| {lbl} | {stats.get(k, 0)} |" for k, lbl in labels])) out.append("Details: [conversion-report.json](conversion-report.json), " "[manifest.json](manifest.json), [Warnungen](diagnostics/warnings.md)") stem = source_name[:-5] if source_name.lower().endswith(".epub") else source_name math = " --mathml" if stats.get("formulas") else "" lua = "" if callouts and CALLOUT_FILTER.is_file(): # '> [!TIP]' boxes: Pandoc needs the filter to draw real boxes lua = f" --lua-filter {shlex.quote(str(CALLOUT_FILTER))}" # book.lua: table of contents after cover/title page/imprint instead of Pandoc's --toc at the very top toc = f" --lua-filter {shlex.quote(str(BOOK_FILTER))}" if BOOK_FILTER.is_file() else " --toc" commands = [ "# Gesamtdatei erzeugen (im selben Arbeitsverzeichnis wie die Konvertierung)", f"python3 epub2md_merge.py {shlex.quote(output_dir)}", "# danach in diesem Ordner:", f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} -o {shlex.quote(stem + '.html')}", f"pandoc {shlex.quote(stem + '.md')} --toc{lua} -o {shlex.quote(stem + '.epub')}", "# PDF über HTML: Pandoc übernimmt dann auch HTML-Tabellen und interne Sprungziele", f"pandoc {shlex.quote(stem + '.md')} -s{math}{lua} -o {shlex.quote(stem + '.html')} && " f"pandoc {shlex.quote(stem + '.html')} --toc{lua} --pdf-engine=xelatex -o {shlex.quote(stem + '.pdf')}", ] if PRINT_CSS.is_file(): commands += [ "# PDF ohne LaTeX mit weasyprint: Farben, Kästen und HTML-Stile bleiben erhalten", f"pandoc {shlex.quote(stem + '.md')} -s{math}{toc}{lua} --css {shlex.quote(str(PRINT_CSS))} " f"--pdf-engine=weasyprint -o {shlex.quote(stem + '.pdf')}", ] out.append("## Weiterverarbeitung") out.append("```bash\n" + "\n".join(commands) + "\n```") if callouts: noun = "Infokasten" if callouts == 1 else "Infokästen" out.append(f"Das Buch enthält {callouts} {noun} (`> [!TIP]` …). Der Filter `callouts.lua` macht daraus " "in HTML, EPUB und PDF gerahmte Kästen mit Titel; ohne ihn zeigt Pandoc „[!TIP]“ als Text.") if stats.get("formulas"): out.append("Das Buch enthält Formeln: Für HTML `--mathml` (oder `--mathjax`) verwenden, sonst belässt " "Pandoc Matrizen und ähnliche Konstrukte als TeX-Text.") out.append("## Reproduktion") out.append("```bash\n" + " ".join(shlex.quote(c) for c in command) + "\n```") return "\n\n".join(out) + "\n" def warnings_md(diag: Diagnostics) -> str: out = ["# Warnungen und Hinweise", ""] levels = Counter(d.level for d in diag.items) out.append(f"Fehler: {levels.get('error', 0)} · Warnungen: {levels.get('warning', 0)} · Hinweise: {levels.get('info', 0)}") if not diag.items: out += ["", "Keine Auffälligkeiten."] return "\n".join(out) + "\n" by_code: dict[str, list[Any]] = {} for d in diag.items: by_code.setdefault(f"{d.level}: {d.code}", []).append(d) order = {"error": 0, "warning": 1, "info": 2} for key in sorted(by_code, key=lambda k: (order.get(k.split(":")[0], 3), k)): items = by_code[key] out += ["", f"## {key} ({len(items)})", ""] for d in items: where = " · ".join(x for x in (d.source, d.chapter) if x) times = f" (×{d.count})" if d.count > 1 else "" out.append(f"- {md_escape(d.message)}{times}" + (f" (`{where}`)" if where else "")) return "\n".join(out) + "\n" def unsupported_md(diag: Diagnostics) -> str: out = ["# Nicht direkt unterstützte Elemente", ""] if not diag.unsupported: out.append("Keine.") else: out.append("Inhalte dieser Elemente wurden als Text übernommen (mit `--keep-html` als HTML).") out += ["", "| Element | Anzahl | Beispiel |", "| --- | ---: | --- |"] for tag, n in diag.unsupported.most_common(): out.append(f"| `{tag}` | {n} | `{diag.unsupported_examples.get(tag, '')}` |") return "\n".join(out) + "\n" def to_json(data: Any) -> str: return json.dumps(data, ensure_ascii=False, indent=2) + "\n" def rel(from_file: str, to: str) -> str: return rel_link(from_file, to) or posixpath.basename(to)