返回 DeepSeek-Reasonix
mathClassify.ts
根目录 / desktop / frontend / src / components / mathClassify.ts
1 export type InlineMathDecision = "math" | "currency" | "literal";
2
3 export interface InlineMathContext {
4 before?: string;
5 after?: string;
6 }
7
8 const PURE_NUMBER = /^\d+(?:,\d{3})*(?:\.\d+)?$/;
9
10 const CURRENCY_BEFORE =
11 /(?:\b(?:costs?|prices?|priced|pay|paid|worth|fees?|budget|salary|total|amount|spend|spent|save|saved|charge|charged|buy|bought|sell|sold|usd|dollars?)\b|(?:价格|售价|花费|成本|预算|金额|合计|总计|总价|单价|支付|付款|收费|价值|卖价|买价|薪资|工资|优惠|节省|美元|美金))\s*(?:(?:is|was|are|were|of|at)\s+|(?:是|为|达|约|共|了)\s*|[::=]\s*)?$/i;
12
13 const CURRENCY_AFTER =
14 /^\s*(?:usd\b|dollars?\b|(?:in\s+)?cash\b|each\b|per\b|\/\s*(?:day|week|month|year)\b|美元|美金|人民币|元|块钱|现金|每(?:个|天|周|月|年))/i;
15
16 const NUMERIC_MATH_BEFORE =
17 /(?:\d(?:,\d{3})*(?:\.\d+)?\s*[-–—−]\s*|(?:[=<>≤≥≈≃~+*/^(,[{]|\\(?:approx|sim|le|ge|lt|gt))\s*)$/;
18
19 const SCIENTIFIC_UNIT_PREFIX = String.raw`(?:da|[qryzafpnumcdhkMGTPEZYRQµμ])?`;
20 const SCIENTIFIC_UNIT_SYMBOL = String.raw`(?:${SCIENTIFIC_UNIT_PREFIX}(?:eV|Hz|mol|cd|Pa|Wb|lm|lx|Bq|Gy|Sv|kat|Wh|Da|bar|m|g|s|A|K|N|J|W|C|V|F|S|T|H|L)|dB|atm|Torr|rpm|bps|Ω|ohms?|bits?|bytes?|minutes?|hours?|min|hr)`;
21 const SCIENTIFIC_UNIT_BOUNDARY = String.raw`(?=$|[\s,.;:!?()[\]{}\/·⋅×^²³⁴⁵⁶⁷⁸⁹⁰-])`;
22 const NUMERIC_MATH_AFTER = new RegExp(
23 String.raw`^\s*(?:[-–—−]\s*\d|${SCIENTIFIC_UNIT_SYMBOL}${SCIENTIFIC_UNIT_BOUNDARY}|°[CFK]${SCIENTIFIC_UNIT_BOUNDARY})`,
24 "i",
25 );
26
27 function currencyContext(context: InlineMathContext): Required<InlineMathContext> {
28 return {
29 before: (context.before ?? "").replace(/[\s()[\]{}"'“”‘’()【】《》〈〉「」『』]+$/u, ""),
30 after: (context.after ?? "").replace(/^[\s()[\]{}"'“”‘’()【】《》〈〉「」『』,;:,;:。、—–-]+/u, ""),
31 };
32 }
33
34 /**
35 * Decide how an `inlineMath` node produced by remark-math should render.
36 *
37 * Pure-number spans remain literal unless their surrounding source provides
38 * positive mathematical context (for example a range endpoint or scientific
39 * unit). High-confidence currency context repairs paired input such as
40 * `costs $5$ today` to the prose token `$5`. Other non-math content is
41 * restored verbatim as text.
42 */
43 export function classifyInlineMath(
44 math: string,
45 context: InlineMathContext = {},
46 ): InlineMathDecision {
47 if (!math || math !== math.trim() || math.includes("\n")) return "literal";
48 if (math.includes("://") || math.includes("](")) return "literal";
49
50 if (PURE_NUMBER.test(math)) {
51 const currency = currencyContext(context);
52 if (CURRENCY_BEFORE.test(currency.before) || CURRENCY_AFTER.test(currency.after)) {
53 return "currency";
54 }
55 if (NUMERIC_MATH_BEFORE.test(context.before ?? "") || NUMERIC_MATH_AFTER.test(context.after ?? "")) {
56 return "math";
57 }
58 return "literal";
59 }
60
61 // A percentage enclosed in explicit delimiters is mathematical notation,
62 // not a currency amount. latexNormalizeForKatex escapes its `%` for KaTeX.
63 if (/^\d+(?:,\d{3})*(?:\.\d+)?%$/.test(math)) return "math";
64
65 // Number followed by variable: implicit multiplication (2.5x, 3y^2).
66 if (/^\d+(?:\.\d+)?[A-Za-z](?:[A-Za-z0-9^_{}]*)?$/.test(math)) return "math";
67 // Number with LaTeX escape: 10\%, 5\cdot3.
68 if (/^\d+(?:\.\d+)?\\(?:%|[A-Za-z]+)(?:\{[^{}]*\})?(?:[A-Za-z0-9\\{}^_+\-*/=<>.()]*)$/.test(math)) return "math";
69
70 // Unary plus/minus: +2, -x, +\alpha, - 3.14.
71 if (/^[+\-]\s*(?:\d+(?:\.\d+)?|[A-Za-z\\])/.test(math)) return "math";
72
73 // LaTeX command (\alpha, \frac{x}{y}, \tfrac12, ...).
74 if (/\\[A-Za-z]+/.test(math)) return "math";
75 if (/[\^_{}|]/.test(math)) return "math";
76 if (/^[+\-=<>±∓]$/.test(math)) return "math";
77 if (/\b(?:alpha|beta|gamma|sum|int|prod|lim|infty|sqrt|frac|sin|cos|tan|log|ln|max|min|partial|nabla|left|right)\b/.test(math)) return "math";
78 if (/^[A-Za-z]{1,6}\s*\([^)]{1,80}\)$/.test(math)) return "math";
79 if (/^[A-Za-z]'{1,}\s*\([^)]{1,80}\)$/.test(math)) return "math";
80 if (/^(?:\(\d+\))+$/.test(math)) return "math";
81 if (/[A-Za-z0-9)\]}]\s*[+\-*/=<>]\s*[+\-]?\s*[A-Za-z0-9([{\\]/.test(math)) return "math";
82
83 // One-sided relation/equality against an implicit operand. The complete
84 // anchors are important: `=1 dollar` must not be accepted by a prefix match.
85 const operand = String.raw`(?:[+\-]?\s*(?:\d+(?:\.\d+)?|[A-Za-z]|\\[A-Za-z]+))`;
86 const relation = String.raw`(?:<=?|>=?|=|≠|≤|≥)`;
87 if (new RegExp(`^(?:${relation}\\s*${operand}|${operand}\\s*${relation})$`).test(math)) return "math";
88
89 if (/^\(?(?:[A-Za-z0-9]|\\[A-Za-z]+)(?:\s*,\s*(?:[A-Za-z0-9]|\\[A-Za-z]+)){1,10}\)?$/.test(math)) return "math";
90 if (/^\[[A-Za-z0-9^_+\-,.\\\s{}]+\]$/.test(math)) return "math";
91
92 if (/[A-Za-z]\s+[A-Za-z]/.test(math)) return "literal";
93 if (/^[A-Z][A-Z0-9_]{1,}$/.test(math)) return "literal";
94 if (/^v\d+(?:\.\d+)*$/i.test(math)) return "literal";
95 if (/^[A-Za-z]{2,}$/.test(math)) return "literal";
96
97 if (/^(?:[A-Za-z]|\\[A-Za-z]+)'{1,}$/.test(math)) return "math";
98 return /^[A-Za-z]$/.test(math) ? "math" : "literal";
99 }
100
101 export function isLikelyInlineMath(math: string): boolean {
102 return classifyInlineMath(math) === "math";
103 }
104
104 lines TYPESCRIPT