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
+21 -10
View File
@@ -54,12 +54,14 @@ def esc(text: object) -> str:
return out.replace(_BACKSLASH_MARK, r"\textbackslash{}")
# 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 doby fałszywe trafienia.
# Markdown inline: pogrubienie+kursywa, pogrubienie, kursywa, kod, linki. Kolejność
# w alternatywie ma znaczenie i jest OD NAJDŁUŻSZEGO znacznika: `***...***` przed
# `**...**` przed `*...*`. Inaczej `***mocne***` złapałoby się jako `**` + zgubiona
# gwiazdka i w PDF zostawałby wisior `*` (realny błąd, który to naprawia). Kursywę
# bierzemy tylko z gwiazdek (nie z podkreśleń): `_` pada w nazwach typu file_name.
_INLINE = re.compile(
r"\*\*(?P<bold>.+?)\*\*"
r"\*\*\*(?P<bolditalic>[^\n]+?)\*\*\*"
r"|\*\*(?P<bold>[^\n]+?)\*\*"
r"|\*(?P<ital>[^*\n]+?)\*"
r"|`(?P<code>[^`\n]+?)`"
r"|\[(?P<ltext>[^\]]+)\]\((?P<lurl>[^)]+)\)"
@@ -73,7 +75,9 @@ def _inline(text: str) -> str:
pos = 0
for m in _INLINE.finditer(text):
out.append(esc(text[pos:m.start()]))
if m.group("bold") is not None:
if m.group("bolditalic") is not None:
out.append(r"\textbf{\textit{" + esc(m.group("bolditalic")) + "}}")
elif 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")) + "}")
@@ -120,15 +124,22 @@ def markdown_to_latex(text: str) -> str:
for ln in lines:
s = ln.strip()
heading = re.match(r"^(#{1,6})\s+(.*)$", s)
# Nagłówek celowo POBŁAŻLIWIE: dowolna liczba kratek na początku, spacja po
# nich NIEobowiązkowa, końcowe kratki (zamknięty ATX `## … ##`) ucinane.
# `\#` w LaTeXu renderuje się jako `#`, więc gdyby `##Bez spacji` trafiło do
# akapitu, w PDF WIDAĆ BY było `##`. Tu żaden znacznik nagłówka nie ostaje.
heading = re.match(r"^(#+)[ \t]*(.*?)[ \t]*#*$", s)
bullet = re.match(r"^[-*+]\s+(.*)$", s)
number = re.match(r"^\d+[.)]\s+(.*)$", s)
if not s:
flush_para(); flush_list()
elif heading:
elif heading: # linia zaczyna się od kratek
flush_para(); flush_list()
cmd = _HEADING_CMD.get(len(heading.group(1)), "paragraph")
out.append(f"\\{cmd}*{{{_inline(heading.group(2))}}}")
content = heading.group(2)
if content: # gołe kratki (bez treści) po prostu pomijamy
level = min(len(heading.group(1)), 6)
cmd = _HEADING_CMD.get(level, "paragraph")
out.append(f"\\{cmd}*{{{_inline(content)}}}")
elif bullet:
flush_para()
if env not in (None, "itemize"):