"""Budowa dokumentu LaTeX raportu (PRE-24). Układ wg prośby partnerów: 1. imię i nazwisko, 2. wprowadzone dane, 3. rysunek kosmogramu, 4. interpretacja natalna, 5. predykcje okresowe. Silnik: XeLaTeX. Nie pdflatex — tekst zawiera polskie znaki ORAZ glify astrologiczne (♄ ♓ ☉ …), a te wymagają silnika unicode'owego z fontem systemowym. DejaVu Sans ma jedno i drugie, dlatego jest fontem głównym. """ from __future__ import annotations # Znaki, które w LaTeXu mają własne znaczenie. # # Odwrotnego ukośnika NIE MA na tej liście celowo. Jego zamiennik # (`\textbackslash{}`) sam zawiera nawiasy klamrowe, więc gdyby szedł w tej samej # pętli, kolejne podmiany uciekłyby nawiasy, które dopiero co wstawiliśmy — # i zamiast ukośnika wyszedłby dosłowny tekst `\textbackslash\{\}`. Dlatego # ukośnik chowamy najpierw pod znacznik zastępczy, a rozwijamy na samym końcu. _ESCAPES = [ ("{", r"\{"), ("}", r"\}"), ("$", r"\$"), ("&", r"\&"), ("#", r"\#"), ("_", r"\_"), ("%", r"\%"), ("~", r"\textasciitilde{}"), ("^", r"\textasciicircum{}"), ] # Znak spoza tekstu użytkownika — nie da się go wpisać, więc nie da się nim # oszukać ucieczki. _BACKSLASH_MARK = "\x00astrololo-backslash\x00" def esc(text: object) -> str: """Ucieka znaki specjalne LaTeXa. Najgroźniejszy jest `%`: bez ucieczki komentuje RESZTĘ LINII, więc tekst od modelu urwałby się w środku zdania i nikt by tego nie zauważył — PDF powstałby normalnie, tylko krótszy. Dlatego to nie kosmetyka, a warunek poprawności raportu. """ out = "" if text is None else str(text) out = out.replace("\\", _BACKSLASH_MARK) # schowaj, żeby nie ucierpiał niżej for ch, rep in _ESCAPES: out = out.replace(ch, rep) return out.replace(_BACKSLASH_MARK, r"\textbackslash{}") def _paragraphs(text: str) -> str: """Tekst od modelu → akapity LaTeXa. Model oddziela akapity pustą linią. Zamieniamy je na osobne akapity, a same złamania wiersza w środku akapitu zostawiamy LaTeXowi do złożenia. """ blocks = [b.strip() for b in str(text or "").split("\n\n")] return "\n\n".join(esc(b).replace("\n", " ") for b in blocks if b) PREAMBLE = r"""\documentclass[11pt,a4paper]{article} \usepackage{fontspec} \usepackage[margin=2.4cm]{geometry} \usepackage{graphicx} % DejaVu Sans ma i polskie znaki, i glify astrologiczne — jeden font na oba % zadania oszczędza nam dobierania fontu zapasowego. \setmainfont{DejaVu Sans} \setlength{\parindent}{0pt} \setlength{\parskip}{0.6em} \pagestyle{plain} \begin{document} """ def build(report: dict, wheel_pdf: str | None = None) -> str: """Składa źródło .tex. `wheel_pdf` to nazwa pliku z kosmogramem (już PDF).""" person = str(report.get("person") or "").strip() data = report.get("data") or {} natal = report.get("natal") or {} predictions = report.get("predictions") or [] parts = [PREAMBLE] # ── 1. imię i nazwisko ────────────────────────────────────────────── parts.append(r"{\Huge\bfseries " + esc(person or "Raport astrologiczny") + r"\par}") parts.append(r"\vspace{0.3em}") # ── 2. wprowadzone dane ───────────────────────────────────────────── rows = [ ("Data urodzenia", data.get("date")), ("Godzina (lokalna)", data.get("time")), ("Offset względem GMT", data.get("tz_offset")), ("Szerokość", data.get("lat")), ("Długość", data.get("lon")), ("Miejsce", data.get("place")), ("System domów", data.get("house_system")), ("Zodiak", data.get("zodiac")), ] parts.append(r"\begin{tabular}{@{}ll@{}}") for label, value in rows: if value not in (None, ""): parts.append(esc(label) + r" & " + esc(value) + r"\\") parts.append(r"\end{tabular}") parts.append(r"\vspace{1em}") # ── 3. kosmogram ──────────────────────────────────────────────────── if wheel_pdf: parts.append(r"\begin{center}") parts.append(r"\includegraphics[width=0.82\textwidth]{" + wheel_pdf + r"}") parts.append(r"\end{center}") parts.append(r"\vspace{0.5em}") # ── 4. interpretacja natalna ──────────────────────────────────────── if natal.get("text"): parts.append(r"\section*{Interpretacja natalna}") parts.append(_paragraphs(natal["text"])) # ── 5. predykcje okresowe ─────────────────────────────────────────── if predictions: parts.append(r"\section*{Predykcje okresowe}") for p in predictions: span = f"{p.get('from_date', '')} – {p.get('to_date', '')}" parts.append(r"\subsection*{" + esc(span) + r"}") parts.append(_paragraphs(p.get("text"))) parts.append(r"\end{document}") return "\n".join(parts) + "\n"