diff --git a/src/lib/MathText.tsx b/src/lib/MathText.tsx
index 049634d..c14cfb2 100644
--- a/src/lib/MathText.tsx
+++ b/src/lib/MathText.tsx
@@ -62,7 +62,73 @@ function Matrix({ src }: { src: string }) {
);
}
+/**
+ * سطرهای ماتریس که خطِبهخط نوشته شدهاند («b₁₁ b₁₂ ⋯ b(1n)» زیرِ «b₂₁ b₂₂ ⋯ b(2n)» و «⋮ ⋮ ⋱ ⋮»):
+ * بدونِ ستونبندی، نقطههای عمودی زیرِ درایهها نمیافتند و فاصلهی دوتایی هم در HTML فرو میریزد.
+ * دو خطِ پشتِ هم یا بیشتر که بدونِ حرفِ فارسیاند و تعدادِ توکنشان برابر (≥۳) است، یک جدول میشوند.
+ * سطرِ اول میتواند برچسب داشته باشد («B(m×n) =»).
+ */
+const PERSIAN = /[-ۿ]/;
+function parseRow(line: string): { label: string; cells: string[] } | null {
+ const t = line.trim();
+ if (!t || PERSIAN.test(t)) return null;
+ const m = /^(.*?\s=\s+)(\S.*)$/.exec(t);
+ const label = m ? m[1].trim() : '';
+ const cells = (m ? m[2] : t).split(/\s+/);
+ // معادله («f(1) = 1² + 1 = 2») سطرِ ماتریس نیست: توکنِ عملگر یعنی ماتریس نیست.
+ // نمادِ تکی (+ = ⊆ → …) جز نقطهچینها و شمارهی فهرست («۲)») هم یعنی ماتریس نیست.
+ if (cells.some((c) => (/^[^\p{L}\p{N}]$/u.test(c) && !'⋯⋮⋱…'.includes(c)) || /^[0-9۰-۹]+[.)]$/.test(c))) return null;
+ return cells.length >= 3 ? { label, cells } : null;
+}
+
+function splitGrids(raw: string): (string | { rows: { label: string; cells: string[] }[] })[] | null {
+ const lines = raw.split('\n');
+ const parts: (string | { rows: { label: string; cells: string[] }[] })[] = [];
+ let buf: string[] = [], found = false, i = 0;
+ while (i < lines.length) {
+ const first = parseRow(lines[i]);
+ let j = i;
+ const rows: { label: string; cells: string[] }[] = [];
+ while (first && j < lines.length) {
+ const r = parseRow(lines[j]);
+ if (!r || r.cells.length !== first.cells.length) break;
+ rows.push(r);
+ j++;
+ }
+ if (rows.length >= 2) {
+ if (buf.length) parts.push(buf.join('\n'));
+ buf = [];
+ parts.push({ rows });
+ found = true;
+ i = j;
+ } else {
+ buf.push(lines[i]);
+ i++;
+ }
+ }
+ if (buf.length) parts.push(buf.join('\n'));
+ return found ? parts : null;
+}
+
+function Grid({ rows }: { rows: { label: string; cells: string[] }[] }) {
+ const border = '2px solid currentColor';
+ const label = rows.find((r) => r.label)?.label ?? '';
+ const clean = (c: string) => plainPersian(plainMath(c));
+ return (
+
+ {label && {clean(label)}}
+
+ {rows.flatMap((r, i) => r.cells.map((c, j) => {clean(c)}))}
+
+
+ );
+}
+
export function MathText({ text: raw, display = false }: { text: string; display?: boolean }) {
+ const grids = splitGrids(raw);
+ if (grids) {
+ return <>{grids.map((p, k) => (typeof p === 'string' ? : ))}>;
+ }
if (!MATRIX.test(raw)) { MATRIX.lastIndex = 0; return ; }
MATRIX.lastIndex = 0;
const out: React.ReactNode[] = [];
@@ -78,7 +144,15 @@ export function MathText({ text: raw, display = false }: { text: string; display
}
function Inline({ raw, display }: { raw: string; display: boolean }) {
+ const first = build(raw, display);
+ // دو فرمولِ بلوکی در یک جمله («a₁ = … و a₂ = …») هر کدام خطِ جدا میگرفتند و «و» و «.» تنها میماندند.
+ const { nodes } = first.blocks > 1 ? build(raw, false) : first;
+ return <>{nodes}>;
+}
+
+function build(raw: string, display: boolean): { nodes: React.ReactNode[]; blocks: number } {
const text = plainPersian(plainMath(raw));
+ let blocks = 0;
const out: React.ReactNode[] = [];
const depth = bracketDepth(text);
let last = 0, i = 0;
@@ -103,6 +177,7 @@ function Inline({ raw, display }: { raw: string; display: boolean }) {
const before = text.slice(0, start + head.length), after = text.slice(start + head.length + run.length);
const inSentence = new RegExp(`[${LETTER}]`).test(before) || new RegExp(`[${LETTER}]`).test(after);
if (display && inSentence && run.trim().length >= 4 && FORMULA.test(run) && depth[start + head.length] === 0) {
+ blocks++;
out.push({run});
} else {
out.push({run});
@@ -111,7 +186,7 @@ function Inline({ raw, display }: { raw: string; display: boolean }) {
}
}
out.push(text.slice(last));
- return <>{out.map((n, j) => (typeof n === 'string' ? {n} : n))}>;
+ return { nodes: out.map((n, j) => (typeof n === 'string' ? {n} : n)), blocks };
}
/** عمقِ پرانتز/کروشه/آکولاد پیش از هر نویسهی متن. */
@@ -158,5 +233,16 @@ function trimUnpaired(run: string): { head: string; run: string } {
head += m;
run = run.slice(m.length);
}
+ // «…اگر i opens(run)) {
+ run = run.slice(0, run.length - /\s*[)\]}]\s*$/.exec(run)![0].length);
+ }
+ // و پرانتزِ بازِ بیجفتِ سرِ تکه («(مثلاً a=7 …» که بستهاش جای دیگری است) هم مالِ جمله است.
+ while (/^\s*[(\[{]/.test(run) && opens(run) > closes(run)) {
+ const m = /^\s*[(\[{]\s*/.exec(run)![0];
+ head += m;
+ run = run.slice(m.length);
+ }
return { head, run };
}
diff --git a/src/lib/lineFit.ts b/src/lib/lineFit.ts
index 941b34f..b014651 100644
--- a/src/lib/lineFit.ts
+++ b/src/lib/lineFit.ts
@@ -15,7 +15,9 @@
const CANDIDATES = [0, -0.02, -0.04, -0.06, -0.08];
const SPACE_EM = 0.26;
const SELECTOR = 'p, li, .whitespace-pre-line';
-const MIN_CHARS = 40;
+const MIN_CHARS = 14;
+// بیشینهی فاصلهی مجازِ بینِ کلمهها نسبت به فاصلهی معمول، پیش از آنکه دوطرفچین کنار گذاشته شود
+const MAX_GAP = 2.2;
const observed = new WeakSet();
const queue = new Set();
@@ -43,6 +45,7 @@ function lineWidths(el: HTMLElement, fontSize: number): number[] {
export function fit(el: HTMLElement) {
if (!el.isConnected) return;
el.style.wordSpacing = '';
+ el.style.textAlign = '';
const cs = getComputedStyle(el);
if (cs.textAlign !== 'justify' || cs.display === 'flex' || cs.display === 'inline-flex' || cs.display === 'none') return;
const text = el.textContent ?? '';
@@ -51,25 +54,33 @@ export function fit(el: HTMLElement) {
const width = el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
if (!(width > 0)) return;
const spaces = (text.match(/\s+/g) ?? []).length;
- if (spaces < 4) return;
+ if (spaces < 2) return;
const s0 = SPACE_EM * fontSize;
el.style.textAlign = 'start';
- let best = 0, bestScore = Infinity;
+ let best = 0, bestScore = Infinity, bestWorst = 0;
for (const c of CANDIDATES) {
el.style.wordSpacing = `${c}em`;
const w = lineWidths(el, fontSize);
if (w.length < 2) { best = 0; break; }
const total = w.reduce((a, b) => a + b, 0);
- let score = 0;
+ let score = 0, worst = 0;
for (let i = 0; i < w.length - 1; i++) {
const k = Math.max(1, (spaces * w[i]) / total); // فاصلههای این خط (تخمین)
const gap = s0 + c * fontSize + Math.max(0, width - w[i]) / k;
// فشردن گرانتر از کشیدن است (چاپِ حرفهای هم همینطور): فشردهشدنِ فاصله زودتر زشت دیده میشود
const dev = (gap - s0) / s0;
+ worst = Math.max(worst, gap / s0);
score += (dev < 0 ? 2.2 * dev : dev) ** 2;
}
- if (score < bestScore - 1e-6) { bestScore = score; best = c; }
+ if (score < bestScore - 1e-6) { bestScore = score; best = c; bestWorst = worst; }
+ }
+ // حتی بهترین شکستن فاصلهای بیش از ۲٫۲ برابرِ معمول میسازد (کلمهی بلند که جا نمیشود): چاپِ
+ // حرفهای هم در این حالت دوطرفچین نمیکند، چون لبهی نامرتب از فاصلهی گشاد بهتر است.
+ if (bestWorst > MAX_GAP) {
+ el.style.wordSpacing = '';
+ el.style.textAlign = 'start';
+ return;
}
el.style.textAlign = '';
el.style.wordSpacing = best ? `${best}em` : '';