fix(render): potrójne gwiazdki i kratki bez spacji nie wyciekają do PDF
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m9s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m59s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 39s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 18s
build-render / build (push) Successful in 8m52s
build / build (push) Successful in 7m9s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 13m40s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 10m3s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 36s
Testy / Kontrola składni wszystkich warstw (push) Successful in 28s

Dwie realne dziury w markdown→LaTeX, obie WIDOCZNE w gotowym PDF:

1. `***mocne***` (pogrubienie+kursywa) łapało się jako `**` + zgubiona gwiazdka →
   `\textbf{*mocne}*`, czyli w druku zostawał wisior `*`. Dokładam alternatywę
   `\*\*\*…\*\*\*` PRZED `**` i `*` (kolejność od najdłuższego znacznika) →
   `\textbf{\textit{…}}`.

2. Nagłówek bez spacji po kratkach (`##Tytuł`), zamknięty ATX (`## Tytuł ##`)
   i 7+ kratek trafiały do akapitu i były eskejpowane jako `\#\#…`. Pułapka: w
   LaTeXu `\#` renderuje się jako `#`, więc w PDF WIDAĆ było `##`, choć w źródle
   `.tex` jest `\#\#`. Dlatego stary test (`"##" not in out`) tego nie łapał —
   podłańcuch `##` nie występuje w `\#\#`. Nagłówki traktujemy teraz pobłażliwie:
   dowolna liczba kratek na starcie, spacja nieobowiązkowa, końcowe kratki
   ucinane, gołe kratki bez treści pomijane. Żaden znacznik nagłówka nie ostaje.

Nowa asercja w testach patrzy pod kątem RENDERU: brak `\#` i brak wiszącej
gwiazdki (poza gwiazdką poleceń `\section*` itd.). +7 testów regresji, render 34.

UWAGA DEPLOY: obraz render w rejestrze to wciąż 56131b20 (sprzed markdown, LOG-27),
bo build-render (TeX Live) padał na runnerze z „no space left on device". Ten PR
dotyka services/render/**, więc powinien wywołać build — ale najpierw trzeba
zwolnić miejsce na runnerze, inaczej i ten build padnie.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #39.
This commit is contained in:
2026-07-28 16:12:52 +02:00
parent 4b17f2dd67
commit 998c83b26e
2 changed files with 82 additions and 10 deletions
+61
View File
@@ -5,6 +5,8 @@ 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 re
import pytest
from app.latex import build, esc
@@ -209,3 +211,62 @@ def test_bold_before_italic_not_two_italics():
def test_empty_and_none_are_safe():
assert md("") == ""
assert md(None) == ""
# ───── regresja: znaczniki, które WIDAĆ w PDF mimo ucieczki (## i ***) ─────
# `\#` renderuje się jako `#`, a zgubiona gwiazdka zostaje `*`. Test na surowym
# `.tex` sprawdzający tylko podłańcuch „##" tego NIE łapie (w źródle jest `\#\#`),
# dlatego pilnujemy wprost: żadnego `\#` i żadnej wiszącej gwiazdki.
def _no_visible_markers(out: str) -> bool:
"""Czy w wyrenderowanym PDF NIE będzie widać znaczników markdown?
`\\#` wyszłoby jako `#`; jedyne dozwolone gwiazdki to część poleceń
`\\section*`/`\\subsection*`/`\\subsubsection*`/`\\paragraph*`."""
if r"\#" in out:
return False
stripped = re.sub(r"\\(?:sub)*section\*|\\paragraph\*", "", out)
return "*" not in stripped
def test_triple_asterisk_becomes_bold_italic():
"""`***tekst***` = pogrubienie + kursywa, bez wiszących gwiazdek."""
out = md("To ***bardzo mocne*** słowo.")
assert r"\textbf{\textit{bardzo mocne}}" in out
assert _no_visible_markers(out)
def test_triple_asterisk_in_list_and_midline():
for src in ("- punkt z ***naciskiem***", "przed ***X*** po"):
assert _no_visible_markers(md(src)), src
def test_heading_without_space_is_converted_not_leaked():
"""`##Bez spacji` też ma zostać nagłówkiem — inaczej `\\#\\#` daje `##` w PDF."""
out = md("##Bez spacji")
assert r"\subsubsection*{Bez spacji}" in out
assert _no_visible_markers(out)
def test_closed_atx_heading_drops_trailing_hashes():
out = md("### Zamknięty ##")
assert r"\paragraph*{Zamknięty}" in out
assert _no_visible_markers(out)
def test_more_than_six_hashes_still_no_hash_leak():
out = md("####### siedem kratek")
assert _no_visible_markers(out)
assert "siedem kratek" in out
def test_bare_hashes_line_is_dropped():
assert md("###") == ""
def test_full_ai_markdown_leaves_no_visible_markers():
"""Realny miks od modelu: nagłówki (ze spacją i bez), potrójne gwiazdki,
zamknięty ATX — nic z tego nie ma być widać w PDF."""
src = ("## Charakter\n\nOsoba ***wybitnie*** wrażliwa.\n\n"
"###Podsekcja bez spacji\n\n- punkt **ważny**\n- i ***kluczowy***\n\n"
"#### Zamknięty ####")
assert _no_visible_markers(md(src))