feat: Markdown→LaTeX w PDF + dopracowanie kosmogramu (PRE-24, PRE-12 etap 4)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m35s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 35s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 19s
build-render / build (push) Successful in 45s
build / build (push) Successful in 50s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 28s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m34s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m35s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 35s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 19s
build-render / build (push) Successful in 45s
build / build (push) Successful in 50s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 28s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Dwie rzeczy w jednym PR (na prośbę).
1) MARKDOWN → LATEX w raporcie PDF (usługa render). Tekst od AI to markdown —
świetny w przeglądarce, ale w PDF `**gwiazdki**` i `## kratki` to dosłowny szum.
Nowy markdown_to_latex zamienia je na PRAWDZIWE formatowanie: nagłówki (degradowane
o poziom, żeby wpadły pod nasz \section „Interpretacja natalna", nie obok),
pogrubienia, kursywy, kod, listy punktowe i numerowane, linki (zostaje sam tekst,
URL bezużyteczny w druku).
Skaner liniowy, nie regex na całości — listy i akapity są wielolinijkowe.
Ucieczka znaków LaTeXa działa TAKŻE wewnątrz formatowania: `**wzrost 50%**` daje
`\textbf{wzrost 50\%}`, a nie zakomentowaną resztę linii. Sprawdzone na wrogim
tekście — `\end{document}` i `\input{}` w treści nie wyrywają się z dokumentu
nawet zawinięte w pogrubienie.
2) KOSMOGRAM — etap 4 (dopracowanie):
- Lots na kole: Fortuna ⊗ (jedyny Lot ze standardowym glifem) na promieniu POD
pierścieniem planet i w akcencie — od razu widać, że to punkt wyliczony, nie
ciało. Loty bez glifu (Spirit, Eros…) pomijamy, żeby nie zaśmiecać.
- Tooltipy: natywny <title> w SVG (bez JS-a). Obiekt → nazwa, pozycja, dom,
retrogradacja. Linia aspektu → „Słońce trygon Mars · orb 0.20°" (polskie nazwy
tylko do dymka; obliczenia trzymają angielskie). Fortuna → nazwa i pozycja.
- Stopnie cuspów przy szprychach. W whole sign cuspy są na 0° znaku — wtedy
pomijamy dwanaście zbędnych zer; w systemach kwadratowych (porphyry) stopień
realnie coś mówi, więc go pokazujemy.
Wszystko działa też w motywie druku (PDF): sprawdzone, że mimo Lotów, dymków
i cuspów SVG nadal nie ma zmiennych CSS.
Weryfikacja wizualna (porphyry): Fortuna przy AC, 12 stopni cuspów, 38 dymków
(24 aspekty + 13 obiektów + Fortuna). Testy: +12 markdown (render), +7 etap 4
(prezentacja). Całość: prezentacja 145, render 27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #37.
This commit is contained in:
@@ -13,6 +13,8 @@ DejaVu Sans ma jedno i drugie, dlatego jest fontem głównym.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
# Znaki, które w LaTeXu mają własne znaczenie.
|
||||
#
|
||||
# Odwrotnego ukośnika NIE MA na tej liście celowo. Jego zamiennik
|
||||
@@ -52,14 +54,96 @@ def esc(text: object) -> str:
|
||||
return out.replace(_BACKSLASH_MARK, r"\textbackslash{}")
|
||||
|
||||
|
||||
def _paragraphs(text: str) -> str:
|
||||
"""Tekst od modelu → akapity LaTeXa.
|
||||
# Markdown inline: pogrubienie, kursywa, kod, linki. Kolejność w alternatywie ma
|
||||
# znaczenie — `**...**` musi być PRZED `*...*`, inaczej pogrubienie rozpadłoby się
|
||||
# na dwie kursywy. Kursywę bierzemy tylko z gwiazdek (nie z podkreśleń): `_` pada
|
||||
# w nazwach typu file_name, a jako kursywa dałoby fałszywe trafienia.
|
||||
_INLINE = re.compile(
|
||||
r"\*\*(?P<bold>.+?)\*\*"
|
||||
r"|\*(?P<ital>[^*\n]+?)\*"
|
||||
r"|`(?P<code>[^`\n]+?)`"
|
||||
r"|\[(?P<ltext>[^\]]+)\]\((?P<lurl>[^)]+)\)"
|
||||
)
|
||||
|
||||
Model oddziela akapity pustą linią. Zamieniamy je na osobne akapity, a same
|
||||
złamania wiersza w środku akapitu zostawiamy LaTeXowi do złożenia.
|
||||
|
||||
def _inline(text: str) -> str:
|
||||
"""Formatowanie w linii → LaTeX. Tekst poza znacznikami jest ESKEJPOWANY;
|
||||
same znaczniki stają się poleceniami, więc ich nawiasy nie są uciekane."""
|
||||
out: list[str] = []
|
||||
pos = 0
|
||||
for m in _INLINE.finditer(text):
|
||||
out.append(esc(text[pos:m.start()]))
|
||||
if m.group("bold") is not None:
|
||||
out.append(r"\textbf{" + esc(m.group("bold")) + "}")
|
||||
elif m.group("ital") is not None:
|
||||
out.append(r"\textit{" + esc(m.group("ital")) + "}")
|
||||
elif m.group("code") is not None:
|
||||
out.append(r"\texttt{" + esc(m.group("code")) + "}")
|
||||
else: # link — w druku URL jest bezużyteczny, zostawiamy sam tekst
|
||||
out.append(esc(m.group("ltext")))
|
||||
pos = m.end()
|
||||
out.append(esc(text[pos:]))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
# Nagłówki od modelu DEGRADUJEMY o poziom: „Interpretacja natalna" to nasza
|
||||
# `\section*`, więc markdownowe `#` ma być pod nią (subsection), nie obok.
|
||||
_HEADING_CMD = {1: "subsection", 2: "subsubsection"}
|
||||
|
||||
|
||||
def markdown_to_latex(text: str) -> str:
|
||||
"""Markdown od modelu → LaTeX. Markdown świetnie czyta się w przeglądarce,
|
||||
ale w PDF `**gwiazdki**` i `## kratki` to dosłowny szum — tu zamieniamy je na
|
||||
prawdziwe formatowanie: nagłówki, pogrubienia, kursywy i listy.
|
||||
|
||||
Skaner liniowy (nie regex na całości), bo listy i akapity są wielolinijkowe
|
||||
i trzeba wiedzieć, gdzie się zaczynają i kończą.
|
||||
"""
|
||||
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)
|
||||
lines = str(text or "").replace("\r\n", "\n").split("\n")
|
||||
out: list[str] = []
|
||||
para: list[str] = []
|
||||
items: list[str] = []
|
||||
env: str | None = None # 'itemize' | 'enumerate'
|
||||
|
||||
def flush_para() -> None:
|
||||
if para:
|
||||
out.append(_inline(" ".join(s.strip() for s in para)))
|
||||
para.clear()
|
||||
|
||||
def flush_list() -> None:
|
||||
nonlocal env
|
||||
if items:
|
||||
body = "\n".join(r"\item " + _inline(it) for it in items)
|
||||
out.append(f"\\begin{{{env}}}\n{body}\n\\end{{{env}}}")
|
||||
items.clear()
|
||||
env = None
|
||||
|
||||
for ln in lines:
|
||||
s = ln.strip()
|
||||
heading = re.match(r"^(#{1,6})\s+(.*)$", s)
|
||||
bullet = re.match(r"^[-*+]\s+(.*)$", s)
|
||||
number = re.match(r"^\d+[.)]\s+(.*)$", s)
|
||||
if not s:
|
||||
flush_para(); flush_list()
|
||||
elif heading:
|
||||
flush_para(); flush_list()
|
||||
cmd = _HEADING_CMD.get(len(heading.group(1)), "paragraph")
|
||||
out.append(f"\\{cmd}*{{{_inline(heading.group(2))}}}")
|
||||
elif bullet:
|
||||
flush_para()
|
||||
if env not in (None, "itemize"):
|
||||
flush_list()
|
||||
env = "itemize"; items.append(bullet.group(1))
|
||||
elif number:
|
||||
flush_para()
|
||||
if env not in (None, "enumerate"):
|
||||
flush_list()
|
||||
env = "enumerate"; items.append(number.group(1))
|
||||
else:
|
||||
flush_list(); para.append(ln)
|
||||
|
||||
flush_para(); flush_list()
|
||||
return "\n\n".join(out)
|
||||
|
||||
|
||||
PREAMBLE = r"""\documentclass[11pt,a4paper]{article}
|
||||
@@ -117,7 +201,7 @@ def build(report: dict, wheel_pdf: str | None = None) -> str:
|
||||
# ── 4. interpretacja natalna ────────────────────────────────────────
|
||||
if natal.get("text"):
|
||||
parts.append(r"\section*{Interpretacja natalna}")
|
||||
parts.append(_paragraphs(natal["text"]))
|
||||
parts.append(markdown_to_latex(natal["text"]))
|
||||
|
||||
# ── 5. predykcje okresowe ───────────────────────────────────────────
|
||||
if predictions:
|
||||
@@ -125,7 +209,7 @@ def build(report: dict, wheel_pdf: str | None = None) -> str:
|
||||
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(markdown_to_latex(p.get("text")))
|
||||
|
||||
parts.append(r"\end{document}")
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
Reference in New Issue
Block a user