Files
MathApp/docs/build_handbook.py
Zareian 7683920b0b docs: handbook, diagrams and guides brought up to 2026-10-02
- technical-handbook: question packs instead of subscriptions, real
  hearts with the new wait screen (screenshot), math-only tutor and what
  is charged (new 9-7), admin cancel/refund, Bazaar planId, account
  deletion, 59 endpoints, 779 tests, 14 apps, status and open decisions.
- Diagrams redrawn: use-case (buy a pack, refunds), data model
  (QuestionCredit, HeartsPass, Profile.birth_date), pack payment sequence.
- The PDF is now built from the Markdown by docs/build_handbook.py
  (pandoc + Chrome, font in docs/assets); the hand-made HTML it used to
  come from was not in the repo and had been lost.
- architecture, operations, security-before-production (item 13 closed),
  ai-cost-and-guardrails and README follow the same changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:10:50 +03:30

96 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
مستندِ فنی را از Markdown به PDF می‌سازد: docs/technical-handbook.md → docs/technical-handbook.pdf
python docs/build_handbook.py
چرا (۱۰ مهر ۱۴۰۵): PDF پیش‌تر از یک HTMLِ دست‌سازِ جدا ساخته می‌شد که در مخزن نبود. هر به‌روزرسانی
یعنی ویرایشِ دو نسخه، و بعد از ۹ مهر آن HTML گم شد. حالا Markdown تنها منبع است و PDF از روی آن
ساخته می‌شود؛ نمودارها PNGهای `docs/images/technical-handbook/` هستند.
نیاز دارد: `pandoc` (brew install pandoc) و Google Chrome. فونت (وزیرمتن، مجوزِ OFL) کنارِ همین فایل در
`docs/assets/` است، پس PDF روی هر ماشینی یکسان درمی‌آید و به اینترنت نیاز ندارد.
"""
import base64
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
DOCS = Path(__file__).resolve().parent
SRC = DOCS / 'technical-handbook.md'
OUT = DOCS / 'technical-handbook.pdf'
CHROME_CANDIDATES = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'google-chrome', 'chromium', 'chromium-browser',
]
CSS = '''
@font-face{font-family:'Vazirmatn';font-weight:100 900;src:url(data:font/woff2;base64,%(font)s) format('woff2')}
@page{size:A4;margin:16mm 15mm 16mm 15mm}
:root{--ink:#1d2330;--muted:#5b6472;--line:#e4e0d6;--accent:#2f55c8;--accent-soft:#eaf0ff;--code-bg:#f6f4ee}
*{box-sizing:border-box}
html,body{background:#fff}
body{margin:0;font-family:Vazirmatn,Tahoma,sans-serif;color:var(--ink);font-size:10.6pt;line-height:1.85;
-webkit-print-color-adjust:exact;print-color-adjust:exact}
h1{font-size:25pt;font-weight:900;margin:0 0 6px;color:var(--ink)}
h1+table{margin-top:14px}
h2{font-size:17pt;font-weight:800;margin:0 0 8px;padding-top:6px;border-top:3px solid var(--accent);break-before:page;break-after:avoid}
h3{font-size:12.5pt;font-weight:700;margin:20px 0 6px;color:#26305a;break-after:avoid}
h4{font-size:11pt;font-weight:700;margin:12px 0 4px;break-after:avoid}
p{margin:6px 0}
a{color:var(--accent);text-decoration:none}
code{font-family:Menlo,Consolas,monospace;font-size:.84em;direction:ltr;unicode-bidi:isolate;background:var(--code-bg);
padding:1px 4px;border-radius:4px;border:1px solid #ece8dd}
pre{direction:ltr;text-align:left;background:#1f2430;color:#e8e6df;border-radius:8px;padding:10px 12px;
font-size:8.6pt;line-height:1.55;white-space:pre-wrap;word-break:break-word;break-inside:avoid}
pre code{background:none;border:0;padding:0;color:inherit;font-size:1em}
table{width:100%%;border-collapse:collapse;margin:8px 0 12px;font-size:9.4pt;break-inside:auto}
th,td{border:1px solid var(--line);padding:5px 8px;text-align:right;vertical-align:top}
th{background:#f4f1e8;font-weight:700}
tr{break-inside:avoid}
blockquote{margin:10px 0;padding:8px 14px;border:1px solid #d9e2fb;border-right:4px solid var(--accent);
background:var(--accent-soft);border-radius:8px;break-inside:avoid}
blockquote h4{margin-top:2px;color:var(--accent)}
img{max-width:100%%;display:block;margin:10px auto 4px;border-radius:6px}
img[src*="screenshot-"]{max-width:62mm;border:1px solid var(--line);box-shadow:0 2px 8px rgba(30,40,80,.08)}
p:has(> img){break-inside:avoid;margin:0}
p > em:only-child{display:block;font-size:9pt;color:var(--muted);text-align:center;margin:2px 0 12px}
ul,ol{padding-right:20px;margin:6px 0}
li{margin:2px 0}
hr{border:0;border-top:1px solid var(--line);margin:14px 0}
'''
def chrome() -> str:
for c in CHROME_CANDIDATES:
if Path(c).exists() or shutil.which(c):
return c
sys.exit('Chrome پیدا نشد؛ مسیرش را در CHROME_CANDIDATES بگذارید.')
def main():
if not shutil.which('pandoc'):
sys.exit('pandoc نصب نیست: brew install pandoc')
font = base64.b64encode((DOCS / 'assets' / 'Vazirmatn.woff2').read_bytes()).decode()
body = subprocess.run(['pandoc', str(SRC), '-f', 'gfm', '-t', 'html5'],
capture_output=True, text=True, check=True).stdout
html = (f'<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">'
f'<title>مستند فنی تدریسینو</title><style>{CSS % {"font": font}}</style></head>'
f'<body>{body}</body></html>')
# فایلِ موقت کنارِ docs تا مسیرِ نسبیِ عکس‌ها (images/…) درست باز شود.
with tempfile.NamedTemporaryFile('w', suffix='.html', dir=DOCS, delete=False, encoding='utf-8') as f:
f.write(html)
tmp = Path(f.name)
try:
subprocess.run([chrome(), '--headless=new', '--disable-gpu', '--no-pdf-header-footer',
'--virtual-time-budget=10000', f'--print-to-pdf={OUT}', tmp.as_uri()],
check=True, capture_output=True)
finally:
tmp.unlink(missing_ok=True)
print(f'{OUT} ({OUT.stat().st_size // 1024} KB)')
if __name__ == '__main__':
main()