Files
MathApp_frontend/src/lib/plainMath.ts

91 lines
6.9 KiB
TypeScript
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.
/**
* تورِ ایمنیِ نمایش: LaTeX و نشانه‌گذاریِ خامی که به هر دلیل به صفحه رسیده به یونیکدِ ساده برمی‌گردد.
* بک‌اند هم همین تبدیل را در seed، دیتابیس و پاسخِ دستیار انجام می‌دهد
* (`content/plain_math.py`، `rag/services/mathtext.py`)؛ این‌جا فقط آخرین لایه است تا متنِ کهنه یا
* پاسخِ ناخواسته‌ی مدل هرگز با `\frac` و `^{2}` دیده نشود. هر دو باید هم‌رفتار بمانند.
*/
const SUP: Record<string, string> = {
...Object.fromEntries([...'۰۱۲۳۴۵۶۷۸۹'].map((c, i) => [c, '⁰¹²³⁴⁵⁶⁷⁸⁹'[i]])),
...Object.fromEntries([...'0123456789'].map((c, i) => [c, '⁰¹²³⁴⁵⁶⁷⁸⁹'[i]])),
'+': '⁺', '-': '⁻', '−': '⁻', '=': '⁼', '(': '⁽', ')': '⁾',
...Object.fromEntries([...'abcdefghijklmnoprstuvwxyzαβγδεθφχ'].map((c, i) => [c, [...'ᵃᵇᶜᵈᵉᶠᵍʰⁱʲᵏˡᵐⁿᵒᵖʳˢᵗᵘᵛʷˣʸᶻᵅᵝᵞᵟᵋᶿᵠᵡ'][i]])),
};
const SUB: Record<string, string> = {
...Object.fromEntries([...'۰۱۲۳۴۵۶۷۸۹'].map((c, i) => [c, '₀₁₂₃₄₅₆₇₈₉'[i]])),
...Object.fromEntries([...'0123456789'].map((c, i) => [c, '₀₁₂₃₄₅₆₇₈₉'[i]])),
'+': '₊', '-': '₋', '−': '₋', '=': '₌', '(': '₍', ')': '₎',
...Object.fromEntries([...'aehijklmnoprstuvx'].map((c, i) => [c, [...'ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]])),
};
const SYMBOLS: Record<string, string> = {
times: '×', cdot: '·', div: '÷', pm: '±', mp: '∓', theta: 'θ', pi: 'π', alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ',
Delta: 'Δ', lambda: 'λ', mu: 'μ', sigma: 'σ', Sigma: 'Σ', phi: 'φ', varphi: 'φ', omega: 'ω', epsilon: 'ε', rho: 'ρ', tau: 'τ',
infty: '∞', neq: '≠', ne: '≠', leq: '≤', le: '≤', geq: '≥', ge: '≥', to: '→', rightarrow: '→', leftarrow: '←',
Rightarrow: '⇒', implies: '⇒', Leftrightarrow: '⇔', iff: '⇔', perp: '⊥', parallel: '∥', circ: '°', degree: '°', ldots: '…',
dots: '…', cdots: '⋯', vdots: '⋮', ddots: '⋱', int: '∫', sum: 'Σ', prod: '∏', partial: '∂', in: '∈', notin: '∉', subset: '⊂', subseteq: '⊆',
supset: '⊃', cup: '∪', cap: '∩', emptyset: '∅', varnothing: '∅', forall: '∀', exists: '∃', angle: '∠', triangle: '△',
approx: '≈', equiv: '≡', sim: '∼', cong: '≅', mid: '|', vert: '|', neg: '¬', therefore: '∴', setminus: '−',
quad: ' ', qquad: ' ', prime: '′',
lim: 'lim', sin: 'sin', cos: 'cos', tan: 'tan', cot: 'cot', sec: 'sec', csc: 'csc', log: 'log', ln: 'ln', exp: 'exp',
max: 'max', min: 'min', det: 'det', gcd: 'gcd', arcsin: 'arcsin', arccos: 'arccos', arctan: 'arctan',
};
const BLACKBOARD: Record<string, string> = { R: 'ℝ', Q: 'ℚ', N: 'ℕ', Z: 'ℤ', C: 'ℂ' };
const SYMBOL_RE = new RegExp('\\\\(' + Object.keys(SYMBOLS).sort((a, b) => b.length - a.length).join('|') + ')', 'g');
const G = '\\{([^{}]*)\\}';
const NEEDS = /[\\^$]|[A-Za-z)]_|\*\*|__|`|^#{1,6}\s/m;
const atomic = (s: string) => /^[\w۰-۹.]+$/.test(s.trim());
const wrap = (s: string) => (atomic(s) ? s.trim() : `(${s.trim()})`);
const tryMap = (body: string, table: Record<string, string>) => {
const b = body.trim();
return b && [...b].every((c) => c in table) ? [...b].map((c) => table[c]).join('') : null;
};
const script = (body: string, table: Record<string, string>, marker: string) => {
const mapped = tryMap(body, table);
if (mapped) return mapped;
const b = body.trim();
if (marker === '^') return ` به توان (${b}) `;
return /^[\w۰-۹]{1,4}$/.test(b) ? b : `_(${b})`;
};
export function plainMath(text: string): string {
if (!text || !NEEDS.test(text)) return text;
let s = text;
s = s.replace(/\$\$([\s\S]+?)\$\$/g, '$1').replace(/(?<![\w$])\$([^$\n]+?)\$(?![\w$])/g, '$1');
s = s.replace(/\\[(\[]\s*|\s*\\[)\]]/g, '');
for (let k = 0; k < 4; k++) {
s = s.replace(new RegExp(`\\\\(?:vec|overrightarrow|overline|bar|hat|widehat)${G}`, 'g'), '$1');
s = s.replace(new RegExp(`\\\\mathbb${G}`, 'g'), (_m, a: string) => BLACKBOARD[a] ?? a);
s = s.replace(new RegExp(`\\\\(?:text|mathrm|mathbf|operatorname|textbf)${G}`, 'g'), '$1');
s = s.replace(new RegExp(`\\\\[dt]?frac${G}${G}`, 'g'), (_m, a: string, b: string) => `${wrap(a)}/${wrap(b)}`);
s = s.replace(new RegExp(`\\\\sqrt${G}`, 'g'), (_m, a: string) => `√${wrap(a)}`);
}
s = s.replace(new RegExp(`\\\\lim\\s*_${G}`, 'g'), 'lim($1)');
s = s.replace(/\\(left|right)\s*/g, '').replace(/\\\|/g, '|').replace(/\\\{/g, '{').replace(/\\\}/g, '}').replace(/\\[, ]/g, ' ');
s = s.replace(/(?<![A-Za-z\\])sqrt\(/g, '√(');
s = s.replace(SYMBOL_RE, (_m, n: string) => SYMBOLS[n]);
// «vdots/ddots» بدونِ بک‌اسلش (در درسِ ماتریس‌ها همین‌طور ذخیره شده بود)؛ سطرِ ماتریس نقطه‌چینِ عمودی/قطری است.
s = s.replace(/(?<![A-Za-z\\])(vdots|ddots)(?![A-Za-z])/g, (_m, n: string) => SYMBOLS[n]);
s = s.replace(new RegExp(`\\^${G}`, 'g'), (_m, a: string) => script(a, SUP, '^'));
s = s.replace(new RegExp(`_${G}`, 'g'), (_m, a: string) => script(a, SUB, '_'));
s = s.replace(/\^\(((?:[^()]|\([^()]*\))*)\)/g, (_m, a: string) => tryMap(a, SUP) ?? ` به توان (${a.trim()}) `);
s = s.replace(/\^([+\-−]?[0-9۰-۹]+)/g, (m, a: string) => tryMap(a, SUP) ?? m);
s = s.replace(/(?<=[A-Za-z0-9۰-۹)₀-₉])\^([A-Za-zα-ωΑ-Ω□])(?![A-Za-z0-9۰-۹])/g, (_m, a: string) => tryMap(a, SUP) ?? ` به توان ${a} `);
s = s.replace(/((?<![A-Za-z0-9_])[A-Za-z]|log|ln|lim|[)∫Σ∏])_([0-9۰-۹]+[A-Za-z]?|[A-Za-z]{1,4}[0-9۰-۹]?)(?![A-Za-z0-9_])/g, (_m, p: string, a: string) => (p === 'log' && [...a].some((c) => !(c in SUB)) ? `log پایه ${a}` : p + [...a].map((c) => SUB[c] ?? c).join('')));
s = s.replace(/\^([+\-−])/g, (_m, a: string) => SUP[a]);
s = s.replace(/(?<=[A-Za-z)'])_([+\-−])/g, (_m, a: string) => SUB[a]);
s = s.replace(/\^(√[0-9۰-۹A-Za-z]+)/g, ' به توان $1 ').replace(/\^([A-Za-z]{2,3})(?![A-Za-z0-9])/g, ' به توان $1 ');
s = s.replace(/(?<=\S) {2,}(?=\S)/g, ' ');
s = s.replace(/\*\*([\s\S]+?)\*\*/g, '$1').replace(/(?<![_\w])__(.+?)__(?![_\w])/g, '$1').replace(/`/g, '');
return s.replace(/^\s{0,3}#{1,6}\s+/gm, '');
}
/**
* بی‌اعراب و بی «ی»ِ اضافه («پایه‌ی سوم» → «پایه سوم»)؛ تنوین («قبلاً») می‌ماند. همان قاعده‌ی بک‌اند
* (`content/plain_math.py::plain_persian`، روی هر پاسخِ API). این‌جا برای پاسخِ استریمیِ دستیار است که پیش از
* پاک‌سازیِ نهاییِ سرور تکه‌تکه می‌رسد.
*/
export function plainPersian(text: string): string {
if (!text) return text;
return text.replace(/[ٌ-ْٔ]/g, '').replace(/ه‌ی(?![؀-ۿ‌])/g, 'ه');
}