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
196 lines
8.3 KiB
Python
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()
|