91 lines
6.9 KiB
TypeScript
91 lines
6.9 KiB
TypeScript
/**
|
||
* تورِ ایمنیِ نمایش: 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, 'ه');
|
||
}
|