""" Verificări mecanice pe diagramele SVG dintr-un fișier de sumarizare. Prinde exact clasele de bug găsite manual în 01_MODUL_1/05_patternuri_candele.md: scară inconsistentă între niveluri, text tăiat de viewBox, text suprapus, linii goale care rup blocul HTML. NU înlocuiește verificarea vizuală — nu vede dacă un text stă peste o linie de preț. Rulează: .venv/bin/python audit_diagrams.py summaries//.md Selftest: .venv/bin/python audit_diagrams.py --selftest """ import re import sys from pathlib import Path CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare) LINE_H_RATIO = 1.07 # înălțimea rândului = font-size efectiv * ratio (același prag ca # LINE_H=13 la 12px, dar scade pentru etichetele de 10px) DEFAULT_FONT = 12.0 # font-size când nu e precizat nici pe element, nici moștenit _TRANSFORM = re.compile(r'translate\(\s*([-\d.]+)\s*[, ]\s*([-\d.]+)\s*\)') _ELEMENT = re.compile( r']*)>||]*)>(.*?)|]*?)/>|<[^>]+>', re.S) _ROLE = re.compile(r'SL\b|stop', re.I) _ROLE_ENTRY = re.compile(r'intrar|entry', re.I) _ROLE_TP = re.compile(r'\bTP\d*|\bțint|\btarget', re.I) def _font_px(css: str) -> float | None: """font-size în px dintr-un atribut/`style` (ex: `font-size:10px`, `font-size="10"`, `font:12px`).""" m = re.search(r'font-size\s*[:=]\s*"?([\d.]+)', css) if m: return float(m.group(1)) m = re.search(r'(?` deschide un cadru; elementele dinăuntru moștenesc deplasarea și font-size-ul, oricât de adânc ar fi imbricate. Font-size-ul de bază vine de pe rădăcina ``. """ root = re.search(r']*)>', svg) base_fs = (_font_px(root.group(1)) if root else None) or DEFAULT_FONT stack = [((0.0, 0.0), base_fs)] for m in _ELEMENT.finditer(svg): if m.group(1) is not None: # t = _TRANSFORM.search(m.group(1)) dx, dy = (float(t.group(1)), float(t.group(2))) if t else (0.0, 0.0) (px, py), fs = stack[-1] own = _font_px(m.group(1)) stack.append(((px + dx, py + dy), own if own else fs)) elif m.group(0) == "": if len(stack) > 1: stack.pop() else: (tx, ty), fs = stack[-1] if m.group(3) is not None: # ... attrs = m.group(2) x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1)) if re.search(r'\bx="', attrs) else 0.0 y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1)) if re.search(r'\by="', attrs) else 0.0 own = _font_px(attrs) yield "text", x + tx, y + ty, attrs, m.group(3), own if own else fs else: # sau alt tag y = re.search(r'\by1="([-\d.]+)"', m.group(4) or "") if y: yield "line", float(y.group(1)) + ty, m.group(4) else: yield "other", 0.0, 0.0 def texts(svg: str): for ev in _elements(svg): if ev[0] != "text": continue _, x, y, attrs, raw, fs = ev inner = re.sub(r'<[^>]+>', '', raw) anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1] w = len(inner) * CHAR_W x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x) yield x0, x0 + w, y, inner.strip(), fs def levels(svg: str): """Perechile line+text care joacă rolurile SL / intrare / TP, cu y absolut.""" out = [] ev = list(_elements(svg)) for a, b in zip(ev, ev[1:]): if a[0] != "line" or b[0] != "text": continue label = re.sub(r'<[^>]+>', '', b[4]).strip() price = re.search(r'(\d+\.\d\d)', label) if not price: continue role = ("sl" if _ROLE.search(label) else "entry" if _ROLE_ENTRY.search(label) else "tp" if _ROLE_TP.search(label) else None) if role: out.append((float(price.group(1)), a[1], role)) return out def _audit_one(i: int, svg: str) -> list[str]: problems = [] vb = re.search(r'viewBox="0 0 ([\d.]+) ([\d.]+)"', svg) if not vb: return [f"svg {i}: fără viewBox"] vw, vh = float(vb.group(1)), float(vb.group(2)) if re.search(r"\n[ \t]*\n", svg): problems.append(f"svg {i}: linie goală înăuntru — markdown rupe blocul") tx = list(texts(svg)) for x0, x1, y, s, _ in tx: if x1 > vw + 2: problems.append(f'svg {i}: text depășește marginea dreaptă ({x1:.0f} > {vw:.0f}): "{s[:45]}"') if y > vh + 2 or y < 0: problems.append(f'svg {i}: text în afara înălțimii (y={y:.0f}, viewBox {vh:.0f}): "{s[:45]}"') for a in range(len(tx)): for b in range(a + 1, len(tx)): xa0, xa1, ya, sa, fa = tx[a] xb0, xb1, yb, sb, fb = tx[b] line_h = LINE_H_RATIO * max(fa, fb) if abs(ya - yb) < line_h and xa0 < xb1 - 4 and xb0 < xa1 - 4: problems.append(f'svg {i}: texte suprapuse — "{sa[:28]}" / "{sb[:28]}"') # dacă diagrama are exact un triplet SL/intrare/TP, raportul distanțelor # în preț trebuie să fie cel din pixeli lv = levels(svg) if len(lv) == 3 and {r for _, _, r in lv} == {"sl", "entry", "tp"}: pts = sorted((p, y) for p, y, _ in lv) (plo, ylo), (pmid, ymid), (phi, yhi) = pts if pmid - plo > 0 and ylo - ymid > 0: rp = (phi - pmid) / (pmid - plo) rx = (ymid - yhi) / (ylo - ymid) if abs(rp - rx) > 0.05: problems.append(f"svg {i}: SCARĂ GREȘITĂ — raport în prețuri {rp:.2f}R, desenat {rx:.2f}R") return problems def check(path: Path) -> list[str]: md = path.read_text(encoding="utf-8") problems = [] for i, svg in enumerate(re.findall(r'', md, re.S)): problems += _audit_one(i, svg) return problems def selftest(): def svg(body): return f'{body}' # două panouri stivuite: fără translație etichetele ar părea suprapuse stacked = svg('DA — panoul de sus' 'NU — panoul de jos') assert not any("suprapuse" in p for p in _audit_one(0, stacked)), "translația nu a fost aplicată" # translație imbricată nested = svg('' 'jos' 'sus') assert not any("suprapuse" in p for p in _audit_one(0, nested)), "translația imbricată nu s-a acumulat" # suprapunerea reală din același panou rămâne semnalată real = svg('unu' 'doi') assert any("suprapuse" in p for p in _audit_one(0, real)), "suprapunerea reală a fost pierdută" # pragul de înălțime urmează font-size-ul: 10px la pas de 12px NU se atinge small = svg('unu' 'doi') assert not any("suprapuse" in p for p in _audit_one(0, small)), "10px la pas de 12px raportat fals" # același caz cu font-size moștenit de pe small_inherited = svg('unu' 'doi') assert not any("suprapuse" in p for p in _audit_one(0, small_inherited)), "font-size moștenit ignorat" # font-size-ul unui text nu se scurge la frații lui (baza vine de pe ) leak = svg('titlu' 'unu' 'doi') assert not any("suprapuse" in p for p in _audit_one(0, leak)), "font-size-ul unui frate s-a scurs" # două rânduri de 10px care chiar se calcă rămân raportate real_small = svg('unu' 'doi') assert any("suprapuse" in p for p in _audit_one(0, real_small)), "suprapunerea reală la 10px a fost pierdută" # triplet SL/intrare/TP cu scară greșită rămâne semnalat bad = svg('SL 90.00' 'intrare 100.00' 'TP 110.00') assert any("SCARĂ" in p for p in _audit_one(0, bad)), "scara greșită nu a fost prinsă" # trei niveluri care NU sunt SL/intrare/TP nu mai declanșează SCARĂ notrip = svg('minimul 99.80' 'cel mai jos 99.90' 'close-ul 100.60') assert not any("SCARĂ" in p for p in _audit_one(0, notrip)), "niveluri care nu sunt triplet declanșează SCARĂ" print("selftest ok") def main(): if "--selftest" in sys.argv: selftest() return 0 files = [Path(a) for a in sys.argv[1:] if a != "--selftest"] or sorted(Path("summaries").glob("*/*.md")) total = 0 for f in files: probs = check(f) total += len(probs) print(f"\n=== {f} — {len(probs)} probleme" if probs else f"\n=== {f} — curat") for p in probs: print(" ", p) print(f"\nTOTAL: {total} probleme în {len(files)} fișiere") return 0 if __name__ == "__main__": sys.exit(main())