""" LaTeX → متنِ ساده‌ی یونیکد، برای متنِ درس‌ها و سوال‌ها. چرا: فرانت (`MathText`) هیچ LaTeX‌ای رندر نمی‌کند؛ هر `\\( \\vec{a} \\)` که در متنِ درس بماند عیناً با بک‌اسلش و آکولاد دیده می‌شود (گزارشِ مالک از درس‌های بردارِ دوازدهم، ۲۰۲۶-۱۰-۱۲). قراردادِ متنِ کتاب‌ها: بردارها بی‌فلش («بردار a»، «|a|»)، توان با نویسه‌ی بالانویس (²، ⁿ). فقط فرمان‌هایی که در seed دیده شد پشتیبانی می‌شود. فرمانِ ناشناس **تغییر نمی‌کند** و `leftover()` آن را برمی‌گرداند تا تست بشکند، نه این‌که بی‌صدا حدس زده شود. """ import re _SUP = {**{str(i): c for i, c in enumerate('⁰¹²³⁴⁵⁶⁷⁸⁹')}, **{'۰۱۲۳۴۵۶۷۸۹'[i]: c for i, c in enumerate('⁰¹²³⁴⁵⁶⁷⁸⁹')}, '+': '⁺', '-': '⁻', '−': '⁻', '=': '⁼', '(': '⁽', ')': '⁾', 'n': 'ⁿ', 'i': 'ⁱ', 'x': 'ˣ', 'y': 'ʸ', 'k': 'ᵏ', 'm': 'ᵐ', 'p': 'ᵖ', 't': 'ᵗ'} _SUB = {**{str(i): c for i, c in enumerate('₀₁₂₃₄₅₆₇₈₉')}, **{'۰۱۲۳۴۵۶۷۸۹'[i]: c for i, c in enumerate('₀₁₂₃₄₅₆₇₈₉')}, '+': '₊', '-': '₋', '−': '₋', '=': '₌', '(': '₍', ')': '₎', 'n': 'ₙ', 'i': 'ᵢ', 'x': 'ₓ', 'k': 'ₖ', 'm': 'ₘ', 'p': 'ₚ', 't': 'ₜ'} _SYMBOLS = { 'times': '×', 'cdot': '·', 'div': '÷', 'pm': '±', 'theta': 'θ', 'pi': 'π', 'alpha': 'α', 'beta': 'β', 'infty': '∞', 'neq': '≠', 'ne': '≠', 'leq': '≤', 'le': '≤', 'geq': '≥', 'ge': '≥', 'to': '→', 'rightarrow': '→', 'Rightarrow': '⇒', 'perp': '⊥', 'parallel': '∥', 'circ': '°', 'ldots': '…', 'dots': '…', 'lim': 'lim', 'sin': 'sin', 'cos': 'cos', 'tan': 'tan', 'log': 'log', 'ln': 'ln', } _BLACKBOARD = {'R': 'ℝ', 'Q': 'ℚ', 'N': 'ℕ', 'Z': 'ℤ', 'C': 'ℂ'} _GROUP = r'\{([^{}]*)\}' def _script(body: str, table: dict, marker: str) -> str: body = body.strip() if body and all(ch in table for ch in body): return ''.join(table[ch] for ch in body) return f'{marker}({body})' def _try(body: str, table: dict): body = body.strip() return ''.join(table[ch] for ch in body) if body and all(ch in table for ch in body) else None def _atomic(s: str) -> bool: return bool(re.fullmatch(r'[\w۰-۹.]+', s.strip())) def _wrap(s: str) -> str: s = s.strip() return s if _atomic(s) else f'({s})' def convert(text: str) -> str: if not isinstance(text, str) or not re.search(r'[\\^]|[A-Za-z]_', text): return text s = text s = re.sub(r'\\[(\[]\s*|\s*\\[)\]]', '', s) # \( \) \[ \] با فاصله‌ی داخلشان for _ in range(4): # آکولادِ تودرتو، از داخل به بیرون s = re.sub(r'\\vec' + _GROUP, r'\1', s) s = re.sub(r'\\(?:overrightarrow|overline|bar|hat)' + _GROUP, r'\1', s) s = re.sub(r'\\mathbb' + _GROUP, lambda m: _BLACKBOARD.get(m[1], m[1]), s) s = re.sub(r'\\(?:text|mathrm|operatorname)' + _GROUP, r'\1', s) s = re.sub(r'\\frac' + _GROUP + _GROUP, lambda m: f'{_wrap(m[1])}/{_wrap(m[2])}', s) s = re.sub(r'\\sqrt' + _GROUP, lambda m: '√' + _wrap(m[1]), s) s = re.sub(r'\\lim\s*_' + _GROUP, r'lim(\1)', s) # lim_{x→0} پایینِ حد است، نه زیرنویس s = re.sub(r'\\(left|right)\s*', '', s) s = s.replace('\\|', '|').replace('\\{', '{').replace('\\}', '}').replace('\\,', ' ').replace('\\ ', ' ') s = re.sub(r'\\([A-Za-z]+)', lambda m: _SYMBOLS.get(m[1], m[0]), s) s = re.sub(r'\^' + _GROUP, lambda m: _script(m[1], _SUP, '^'), s) s = re.sub(r'_' + _GROUP, lambda m: _script(m[1], _SUB, '_'), s) # شکل‌های بی‌آکولاد که کتاب‌ها و مدل‌ها نوشته‌اند: a^2، a^n، a^(m-n)، x_1. هر چه نویسه‌ی # بالانویس/زیرنویس ندارد (a^b، D_f) همان‌طور می‌ماند. «___» (جای خالی) دست نمی‌خورد. s = re.sub(r'\^\(([^()]*)\)', lambda m: _try(m[1], _SUP) or m[0], s) s = re.sub(r'\^([+\-−]?[0-9۰-۹]+|[nixykmpt])(?![A-Za-z0-9۰-۹])', lambda m: _try(m[1], _SUP) or m[0], s) s = re.sub(r'(?<=[A-Za-z\)])_([0-9۰-۹]+|[nikmpt])(?![A-Za-z0-9۰-۹])', lambda m: _try(m[1], _SUB) or m[0], s) return s _LEFTOVER = re.compile(r'\\[A-Za-z]+|\\[()\[\]]|\$|[_^]\{') def leftover(text: str) -> list[str]: return _LEFTOVER.findall(text) if isinstance(text, str) else [] # کلیدهایی که در `Lesson.figures` متنِ نمایشی‌اند. `expr` (فرمولِ نمودار) و نامِ نقطه‌ها # (`point`/`from`/`to`) هرگز نباید بازنویسی شوند: `x^2` در expr را به «x²» ببری نمودار می‌شکند. FIGURE_TEXT_KEYS = frozenset({'title', 'text', 'label', 'labels'}) def convert_figures(obj, _key=None): """فقط رشته‌های زیرِ `FIGURE_TEXT_KEYS` را (در هر عمقی) تبدیل می‌کند.""" if isinstance(obj, str): return convert(obj) if _key in FIGURE_TEXT_KEYS else obj if isinstance(obj, list): return [convert_figures(v, _key) for v in obj] if isinstance(obj, dict): return {k: convert_figures(v, k) for k, v in obj.items()} return obj