MathText: last-line guard that turns stray LaTeX/markdown into plain Unicode
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { plainMath } from './plainMath';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* متنِ فارسی که ریاضی لایش است («عدد −۱ میشود»، «رأس (۱، −۴) است»).
|
* متنِ فارسی که ریاضی لایش است («عدد −۱ میشود»، «رأس (۱، −۴) است»).
|
||||||
@@ -32,7 +33,8 @@ const EDGE_END = /[\s،؛:!؟«»"'.=]+$/;
|
|||||||
// سمتِ خودشان میکشیدند، پس فهرست باز برعکس میشد.
|
// سمتِ خودشان میکشیدند، پس فهرست باز برعکس میشد.
|
||||||
const LTR = { unicodeBidi: 'isolate-override', direction: 'ltr' } as const;
|
const LTR = { unicodeBidi: 'isolate-override', direction: 'ltr' } as const;
|
||||||
|
|
||||||
export function MathText({ text }: { text: string }) {
|
export function MathText({ text: raw }: { text: string }) {
|
||||||
|
const text = plainMath(raw);
|
||||||
const out: React.ReactNode[] = [];
|
const out: React.ReactNode[] = [];
|
||||||
const depth = bracketDepth(text);
|
const depth = bracketDepth(text);
|
||||||
let last = 0, i = 0;
|
let last = 0, i = 0;
|
||||||
|
|||||||
77
src/lib/plainMath.ts
Normal file
77
src/lib/plainMath.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* تورِ ایمنیِ نمایش: 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: '⋯', 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(SYMBOL_RE, (_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-z)∫Σ∏])_([0-9۰-۹]+|[A-Za-z]{1,4}[0-9۰-۹]?)/g, (_m, a: string) => [...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, '');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user