"""Generowanie źródła LaTeX raportu (PRE-24). Najważniejsza część to UCIECZKA znaków specjalnych. Kompilacji tu nie uruchomimy (brak TeX Live w tym środowisku), ale to i tak nie ona jest tu najbardziej krucha: błędna ucieczka nie wywala kompilacji, tylko po cichu psuje treść — najgorszy możliwy wynik, bo PDF powstaje i wygląda poprawnie. """ import pytest from app.latex import build, esc # ─────────────────────────── ucieczka znaków specjalnych ──────────────── def test_percent_is_escaped(): """NAJGROŹNIEJSZY przypadek: `%` bez ucieczki komentuje resztę linii, więc zdanie urywa się w środku, a PDF powstaje normalnie — tylko krótszy.""" out = esc("wzrost o 50% w tym okresie") assert r"50\%" in out assert "w tym okresie" in out def test_backslash_is_escaped_first(): """Odwrotny ukośnik musi iść pierwszy, inaczej zepsulibyśmy ucieczki wstawione później (np. `\\%` zamieniłoby się w coś innego).""" assert esc("a\\b") == r"a\textbackslash{}b" def test_all_specials_are_escaped(): for ch in ("$", "&", "#", "_", "%", "{", "}"): assert ch not in esc(ch).replace("\\" + ch, ""), f"{ch} nie zostało uciec­zone" def test_tilde_and_caret_use_commands(): assert esc("~") == r"\textasciitilde{}" assert esc("^") == r"\textasciicircum{}" def test_polish_and_glyphs_pass_through(): """XeLaTeX bierze unicode wprost — polskich znaków ani glifów nie ruszamy.""" text = "Zażółć gęślą jaźń ♄ ♓ ☉" assert esc(text) == text def test_escape_handles_none_and_numbers(): assert esc(None) == "" assert esc(42) == "42" # ─────────────────────────────── układ dokumentu ───────────────────────── def _report(**over): base = { "person": "Jan Kowalski", "data": {"date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.0647, "lon": 19.945, "place": "Kraków", "house_system": "whole_sign", "zodiac": "tropical"}, "natal": {"text": "Interpretacja natalna.\n\nDrugi akapit."}, "predictions": [ {"from_date": "2026-01-01", "to_date": "2026-03-31", "text": "Pierwszy kwartał."}, {"from_date": "2026-07-01", "to_date": "2026-09-30", "text": "Trzeci kwartał."}, ], } base.update(over) return base def test_document_is_complete(): tex = build(_report()) assert tex.startswith(r"\documentclass") assert tex.rstrip().endswith(r"\end{document}") def test_uses_unicode_engine_setup(): """XeLaTeX + fontspec — bez tego polskie znaki i glify nie wyjdą.""" tex = build(_report()) assert r"\usepackage{fontspec}" in tex assert r"\setmainfont" in tex def test_section_order_matches_the_brief(): """Prośba partnerów: imię → dane → rysunek → natalna → predykcje.""" tex = build(_report(), wheel_pdf="wheel.pdf") person = tex.index("Jan Kowalski") data = tex.index("Data urodzenia") wheel = tex.index("includegraphics") natal = tex.index("Interpretacja natalna") preds = tex.index("Predykcje okresowe") assert person < data < wheel < natal < preds def test_person_name_opens_the_document(): tex = build(_report()) body = tex.split(r"\begin{document}")[1] assert body.index("Jan Kowalski") < 80, "imię ma być na samym początku" def test_all_predictions_are_included(): tex = build(_report()) assert "2026-01-01" in tex and "2026-07-01" in tex assert "Pierwszy kwartał" in tex and "Trzeci kwartał" in tex def test_paragraphs_are_preserved(): """Model oddziela akapity pustą linią — mają zostać akapitami, nie zlepkiem.""" tex = build(_report()) assert "Interpretacja natalna." in tex and "Drugi akapit." in tex assert "Interpretacja natalna.\n\nDrugi akapit." in tex def test_hostile_text_cannot_break_the_document(): """Tekst od modelu jest wejściem z ZEWNĄTRZ — nie może wstrzyknąć polecenia ani urwać dokumentu.""" nasty = r"100% \end{document} \input{/etc/passwd} $x_1$ #& {}" tex = build(_report(natal={"text": nasty})) body = tex.split(r"\begin{document}")[1] assert body.count(r"\end{document}") == 1, "tekst urwał dokument" assert r"\input{" not in body def test_missing_parts_are_simply_absent(): """Niekompletny materiał ma dać krótszy raport, nie wyjątek.""" tex = build({"person": "", "data": {}}) assert r"\begin{document}" in tex and r"\end{document}" in tex assert "Raport astrologiczny" in tex # zapas, gdy brak imienia def test_wheel_is_optional(): assert "includegraphics" not in build(_report()) assert "includegraphics" in build(_report(), wheel_pdf="wheel.pdf") # ─────────────────── Markdown → LaTeX (nie surowy copy-paste) ───────────── from app.latex import markdown_to_latex as md def test_bold_and_italic_become_commands(): out = md("To **ważne** i *podkreślone*.") assert r"\textbf{ważne}" in out assert r"\textit{podkreślone}" in out assert "**" not in out and "*" not in out # znaczniki znikają def test_headings_are_demoted_under_our_section(): """Markdownowe nagłówki idą POD nasz `\\section*` (natalna/predykcje): # → subsection, ## → subsubsection, ### i głębsze → paragraph.""" assert r"\subsection*{Tytuł}" in md("# Tytuł") assert r"\subsubsection*{Podtytuł}" in md("## Podtytuł") assert r"\paragraph*{Głębiej}" in md("### Głębiej") assert "#" not in md("## Podtytuł") # kratki znikają def test_bullet_list_becomes_itemize(): out = md("- pierwszy\n- drugi\n- trzeci") assert r"\begin{itemize}" in out and r"\end{itemize}" in out assert out.count(r"\item ") == 3 def test_numbered_list_becomes_enumerate(): out = md("1. raz\n2. dwa") assert r"\begin{enumerate}" in out and r"\end{enumerate}" in out assert out.count(r"\item ") == 2 def test_switching_list_type_closes_the_previous(): """Punktory po numerach nie mogą wpaść do jednego środowiska.""" out = md("1. numer\n- punkt") assert r"\end{enumerate}" in out and r"\begin{itemize}" in out def test_inline_code_becomes_texttt(): assert r"\texttt{kod}" in md("użyj `kod` tutaj") def test_link_keeps_text_drops_url(): out = md("zobacz [stronę](https://example.com/x)") assert "stronę" in out assert "example.com" not in out and "http" not in out def test_paragraphs_are_separated(): out = md("Pierwszy akapit.\n\nDrugi akapit.") assert "Pierwszy akapit." in out and "Drugi akapit." in out assert out.count("\n\n") >= 1 def test_special_chars_inside_formatting_are_escaped(): """Ucieczka LaTeXa działa TAKŻE wewnątrz pogrubienia — inaczej `%` w bold zakomentowałby resztę linii.""" out = md("**wzrost 50% & więcej**") assert r"\textbf{wzrost 50\% \& więcej}" in out def test_no_markdown_markers_leak_through(): """Żaden surowy znacznik markdown nie ma trafić do PDF.""" out = md("## Tytuł\n\n**b** i *i* oraz `c`\n\n- lista") for marker in ("##", "**", "`"): assert marker not in out def test_bold_before_italic_not_two_italics(): """`**x**` to pogrubienie, nie dwie kursywy — kolejność w regexie.""" out = md("**mocno**") assert r"\textbf{mocno}" in out assert r"\textit{" not in out def test_empty_and_none_are_safe(): assert md("") == "" assert md(None) == ""