Files
atm-curs-dl/render_html.py
Claude Agent c28e585aa0 render_html: stil de tabel — chenare, antet gri, randuri alternate
Userul a semnalat ca tabelele sunt greu de citit. Cauza: CSS-ul n-avea
niciun stil de tabel, deci markdown-ul iesea fara chenare si fara fundal.
Se repara o data, pentru toate cele 114 pagini.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-09-19 18:03:44 +00:00

196 lines
8.3 KiB
Python

"""
Render every summaries/**/*.md to a sibling .html — mobile-readable (viewport
meta + real font size), served alongside the .md via `tailscale serve`.
Also (re)writes summaries/index.html linking every rendered page.
Run after adding/editing any summary.
"""
import re
from html import escape
from pathlib import Path
import markdown
ROOT = Path(__file__).parent / "summaries"
PAGE_TEMPLATE = """<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
body {{ max-width: 720px; margin: 0 auto; padding: 16px 20px 60px;
font: 18px/1.55 -apple-system, Segoe UI, Roboto, sans-serif;
color: #1a1a1a; }}
h1 {{ font-size: 1.4em; }}
h2 {{ font-size: 1.15em; margin-top: 1.6em; border-top: 1px solid #ddd; padding-top: .8em; }}
code {{ font-size: .95em; background: #f0f0f0; padding: .1em .3em; border-radius: 3px; }}
a {{ color: #1a5fb4; }}
hr {{ border: none; border-top: 1px solid #ddd; margin: 2em 0; }}
em {{ color: #555; }}
table {{ border-collapse: collapse; width: 100%; margin: 1.4em 0;
font-size: .92em; display: block; overflow-x: auto; }}
th, td {{ border: 1px solid #ccc; padding: .5em .7em; text-align: left;
vertical-align: top; }}
th {{ background: #eceff3; font-weight: 600; }}
tbody tr:nth-child(even) td {{ background: #f7f8fa; }}
nav {{ font-size: .8em; color: #888; line-height: 1.5; }}
nav a {{ color: #777; text-decoration: none; }}
nav a:hover {{ text-decoration: underline; }}
.crumbs {{ padding-bottom: .5em; border-bottom: 1px solid #eee; }}
.crumbs .sep {{ margin: 0 .35em; color: #bbb; }}
.pager {{ margin-top: 3em; padding-top: 1em; border-top: 1px solid #ddd;
display: flex; flex-wrap: wrap; gap: .6em 1em; justify-content: space-between; }}
.pager a {{ overflow-wrap: anywhere; }}
.pager .next {{ text-align: right; margin-left: auto; }}
</style>
</head>
<body>
{body}
</body>
</html>
"""
def check_svgs(md_path: Path, text: str) -> None:
"""O linie goală în interiorul unui <svg> face python-markdown să rupă
blocul HTML în paragrafe, iar browserul aruncă restul elementelor —
diagrama se randează pe jumătate, fără nicio eroare. S-a întâmplat deja
de două ori; mai bine crapă aici decât să publici o diagramă stricată."""
for m in re.finditer(r"<svg .*?</svg>", text, re.S):
if re.search(r"\n[ \t]*\n", m.group(0)):
line = text[: m.start()].count("\n") + 1
raise SystemExit(f"{md_path}:{line}: linie goală în interiorul <svg> — markdown va rupe blocul")
MODULE_NAMES = {"09_QA": "Q&A", "16_ANALIZA_FUNDAMENTALA": "Analiză fundamentală",
"08_ANALIZA_ZILNICA": "Analiză zilnică",
"02_CATALIZATORI": "Catalizatori",
"17_STRATEGII": "Strategii"}
def module_name(folder: str) -> str:
"""03_MODUL_2 -> Modulul 2; nume speciale în MODULE_NAMES."""
if folder in MODULE_NAMES:
return MODULE_NAMES[folder]
rest = folder.split("_", 1)[-1]
if rest.startswith("MODUL_"):
return "Modulul " + rest[6:]
return rest.replace("_", " ").capitalize()
def md_title(md_path: Path) -> str:
text = md_path.read_text(encoding="utf-8")
return text.splitlines()[0].lstrip("# ").strip() if text else md_path.stem
def short_title(title: str, date_only: bool = False) -> str:
"""Taie prefixul de modul din titlu ("Modul 2 / #6 — ..." -> "#6 — ..."),
pentru locurile unde modulul e deja scris alături: breadcrumb, bara de
navigare, titlurile din index. `date_only` ține doar "Q&A <data>", pentru
barele înguste de pe telefon — subiectul se vede oricum în H1, dedesubt."""
short = re.sub(r"^(?:[^/]*/\s*(?=#)|Q&A (?=\d))", "", title)
return short.split(" — ")[0] if date_only else short
SUBFOLDER_NAMES = {"fise": "fișe"}
def nav_html(md_path: Path, title: str) -> tuple[str, str]:
"""Breadcrumb sus + înainte/înapoi jos. Vecinii vin din glob pe folder,
ca render_file să meargă apelat singur. Doar fișierele numerotate
(lecțiile) intră în secvență — INDEX_TEMATIC nu. Fișierele cu un nivel
în plus (ex. 17_STRATEGII/fise/*.md) primesc un segment suplimentar de
breadcrumb și link-uri către index cu un "../" în plus."""
rel_parts = md_path.relative_to(ROOT).parts
up = "../" * (len(rel_parts) - 1)
folder = rel_parts[0]
subfolder = rel_parts[1] if len(rel_parts) == 3 else None
sep = '<span class="sep">/</span>'
crumbs = [f'<a href="{up}index.html">Sumarizări</a>',
f'<a href="{up}index.html#{folder}">{escape(module_name(folder))}</a>']
if subfolder:
crumbs.append(escape(SUBFOLDER_NAMES.get(subfolder, subfolder)))
if folder == "09_QA" and md_path.stem != "INDEX_TEMATIC":
crumbs.append('<a href="INDEX_TEMATIC.html">index tematic</a>')
crumbs.append(escape(short_title(title, date_only=folder == "09_QA")))
top = f'<nav class="crumbs">{sep.join(crumbs)}</nav>'
lessons = sorted(f for f in md_path.parent.glob("*.md") if f.name[0].isdigit())
prev_a = next_a = ""
if md_path in lessons:
i = lessons.index(md_path)
label = lambda f: f"{f.name.split('_')[0]}. {escape(short_title(md_title(f), date_only=folder == '09_QA'))}"
if i > 0:
prev_a = f'<a class="prev" href="{lessons[i-1].stem}.html">← {label(lessons[i-1])}</a>'
if i < len(lessons) - 1:
next_a = f'<a class="next" href="{lessons[i+1].stem}.html">{label(lessons[i+1])} →</a>'
bottom = (f'<nav class="pager">{prev_a}'
f'<a class="up" href="{up}index.html#{folder}">↑ index</a>{next_a}</nav>')
return top, bottom
def link_md_to_html(md_path: Path, body: str) -> str:
"""Trimiterile din .md duc la .md (ca să meargă și pe Gitea); în pagină
trebuie să ducă la .html. Doar cele care rămân în summaries/ — un link
spre triaj/ sau harvest/ n-are pagină randată. Acceptă și un nivel în
plus (module/fise/x.md), care tot are pagină randată."""
def sub(m):
tinta = (md_path.parent / (m.group(1) + ".md")).resolve()
randata = tinta.parent.parent == ROOT or tinta.parent.parent.parent == ROOT
if randata and tinta.exists(): # doar paginile randate
return f'href="{m.group(1)}.html{m.group(2) or ""}"'
return m.group(0)
return re.sub(r'href="(?!https?:)([^":]+?)\.md(#[^"]*)?"', sub, body)
def render_file(md_path: Path) -> tuple[Path, str]:
text = md_path.read_text(encoding="utf-8")
check_svgs(md_path, text)
body = markdown.markdown(text, extensions=["extra", "toc"])
body = link_md_to_html(md_path, body)
title = text.splitlines()[0].lstrip("# ").strip() if text else md_path.stem
top, bottom = nav_html(md_path, title)
html_path = md_path.with_suffix(".html")
html_path.write_text(PAGE_TEMPLATE.format(title=title, body=top + body + bottom), encoding="utf-8")
return html_path, title
def write_index(pages: list[tuple[Path, str]]):
"""Grupat pe module, cu titlul H1 al fiecărei pagini — numele fișierului
(o dată, un număr) nu spune nimic despre ce e înăuntru."""
sections = []
current = None
for path, title in sorted(pages):
if path.parent.name != current:
if current is not None:
sections.append("</ul>")
current = path.parent.name
sections.append(f'<h2 id="{current}">{escape(module_name(current))}</h2><ul>')
sections.append(f'<li><a href="{path.relative_to(ROOT)}">{escape(short_title(title))}</a></li>')
if current is not None:
sections.append("</ul>")
(ROOT / "index.html").write_text(
PAGE_TEMPLATE.format(title="ATM — Sumarizări",
body="<h1>ATM — Sumarizări</h1>" + "".join(sections)),
encoding="utf-8",
)
def main():
md_files = sorted(ROOT.glob("*/*.md"))
pages = [render_file(f) for f in md_files]
# Fișele (module/fise/*.md) sunt material-sursă citat din capitole, nu
# intră în index.html — se randează, dar nu se listează.
fise_files = sorted(ROOT.glob("*/fise/*.md"))
for f in fise_files:
render_file(f)
write_index(pages)
print(f"Rendered {len(pages) + len(fise_files)} page(s) + index.html")
if __name__ == "__main__":
main()