/** 题目 HTML 处理(与学生端 doWork/Question 保持一致) */ const LATEX_RENDER_URL = 'https://latex.codecogs.com/svg.image?'; export const fixMathSymbolsInHtml = (html: string): string => { if (html == null || typeof html !== 'string') return ''; let t = html; t = t.replace(/\uFE63/g, '\u2212'); t = t.replace(/<(?![a-zA-Z/!])/g, '<'); t = t.replace(/ > /g, ' > '); t = t.replace(/AUB/g, 'A∪B'); return t; }; const latexToImageUrl = (latex: string): string => { if (!latex) return ''; let formula = latex.trim(); const punctuationMap: Record = { ',': ',', '。': '.', ':': ':', ';': ';', '!': '!', '?': '?', '(': '(', ')': ')', '【': '[', '】': ']', '{': '{', '}': '}', '《': '<', '》': '>', "\u201c": '"', "\u201d": '"', "\u2018": "'", "\u2019": "'", '+': '+', '-': '-', '\u2212': '-', '×': '*', '÷': '/', '=': '=', '<': '<', '>': '>', '%': '%', '&': '&', '*': '*', '@': '@', '#': '#', '$': '$', '^': '^', '~': '~', '|': '|', '\': '\\', '/': '/', ' ': ' ', }; for (const [cn, en] of Object.entries(punctuationMap)) { formula = formula.split(cn).join(en); } const mathSymbolMap: Record = { '∈': ' \\in ', '∉': ' \\notin ', '⊂': ' \\subset ', '⊃': ' \\supset ', '⊆': ' \\subseteq ', '⊇': ' \\supseteq ', '∪': ' \\cup ', '∩': ' \\cap ', '∅': ' \\emptyset ', '≤': ' \\leq ', '≥': ' \\geq ', '≠': ' \\neq ', '≈': ' \\approx ', '≡': ' \\equiv ', '∝': ' \\propto ', '±': ' \\pm ', '∓': ' \\mp ', '×': ' \\times ', '÷': ' \\div ', '·': ' \\cdot ', '∘': ' \\circ ', 'α': '\\alpha ', 'β': '\\beta ', 'γ': '\\gamma ', 'δ': '\\delta ', 'ε': '\\varepsilon ', 'θ': '\\theta ', 'λ': '\\lambda ', 'μ': '\\mu ', 'σ': '\\sigma ', 'φ': '\\varphi ', 'ω': '\\omega ', 'Δ': '\\Delta ', 'Σ': '\\Sigma ', 'Ω': '\\Omega ', '∞': '\\infty ', '∂': '\\partial ', '∇': '\\nabla ', '∫': '\\int ', '∑': '\\sum ', '∏': '\\prod ', '√': '\\sqrt ', '∠': '\\angle ', '⊥': '\\perp ', '∥': '\\parallel ', '→': '\\to ', '⇒': '\\Rightarrow ', '⇔': '\\Leftrightarrow ', '∀': '\\forall ', '∃': '\\exists ', '¬': '\\neg ', '∧': '\\land ', '∨': '\\lor ', }; for (const [symbol, latexCmd] of Object.entries(mathSymbolMap)) { formula = formula.split(symbol).join(latexCmd); } formula = formula.replace(/\b([RNZQC])\+/g, '$1^+'); formula = formula.replace(/\b([RNZQC])\*/g, '$1^*'); formula = formula .replace(/\\overset\s*\{\s*\\rightarrow\s*\}\s*\{([^}]+)\}/g, '\\vec{$1}') .replace(/\\text\s*\{\s*π\s*\}/g, '\\pi') .replace(/\\text\s*\{\s*sin\s*\}/gi, '\\sin') .replace(/\\text\s*\{\s*cos\s*\}/gi, '\\cos') .replace(/\\text\s*\{\s*tan\s*\}/gi, '\\tan') .replace(/\\text\s*\{\s*log\s*\}/gi, '\\log') .replace(/π/g, '\\pi'); const encoded = encodeURIComponent(formula); return `${LATEX_RENDER_URL}\\inline&space;\\dpi{200}&space;${encoded}`; }; const isSimpleFormula = (formula: string): boolean => { const complexPatterns = [ /\\frac\b/, /\\sqrt\b/, /\\sum\b/, /\\int\b/, /\\prod\b/, /\\lim\b/, /\\vec\b/, /\\overline\b/, /\\underline\b/, /\\overset\b/, /\\underset\b/, /\\binom\b/, /\\matrix\b/, /\\begin\b/, /\^{[^}]+}/, /_{[^}]+}/, ]; return !complexPatterns.some((pattern) => pattern.test(formula)); }; const simpleFormulaToHtml = (formula: string): string => { let text = formula; text = text.replace(/\\text\s*\{([^}]*)\}/g, '$1'); const greekMap: Record = { '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', '\\epsilon': 'ε', '\\varepsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ', '\\mu': 'μ', '\\pi': 'π', '\\sigma': 'σ', '\\phi': 'φ', '\\varphi': 'φ', '\\omega': 'ω', '\\Delta': 'Δ', '\\Sigma': 'Σ', '\\Omega': 'Ω', }; for (const [latex, symbol] of Object.entries(greekMap)) { text = text.split(latex).join(symbol); } text = text.replace(/\\sin\b/g, 'sin'); text = text.replace(/\\cos\b/g, 'cos'); text = text.replace(/\\tan\b/g, 'tan'); text = text.replace(/\\log\b/g, 'log'); text = text.replace(/\\ln\b/g, 'ln'); text = text.replace(/\\infty\b/g, '∞'); text = text.replace(/\\pm\b/g, '±'); text = text.replace(/\\times\b/g, '×'); text = text.replace(/\\div\b/g, '÷'); text = text.replace(/\\leq\b/g, '≤'); text = text.replace(/\\geq\b/g, '≥'); text = text.replace(/\\neq\b/g, '≠'); text = text.replace(/\\in\b/g, '∈'); text = text.replace(/\\subset\b/g, '⊂'); text = text.replace(/\\cup\b/g, '∪'); text = text.replace(/\\cap\b/g, '∩'); text = text.replace(/\\emptyset\b/g, '∅'); text = text.replace(/\\cdot\b/g, '·'); text = text.replace(/\\to\b/g, '→'); const superscriptMap: Record = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '+': '⁺', '-': '⁻', 'n': 'ⁿ', }; const subscriptMap: Record = { '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉', '+': '₊', '-': '₋', 'n': 'ₙ', 'i': 'ᵢ', 'k': 'ₖ', }; text = text.replace(/\^([0-9n+\-])/g, (_, char) => superscriptMap[char] || `^${char}`); text = text.replace(/_([0-9nik+\-])/g, (_, char) => subscriptMap[char] || `_${char}`); text = text.replace(/\\left\s*/g, ''); text = text.replace(/\\right\s*/g, ''); text = text.replace(/\\/g, ''); return `${text}`; }; export const processFormula = (html: string): string => { if (html == null || typeof html !== 'string') return ''; if (!html) return ''; const formulaRegex = /"']+|"[^"]*"|'[^']*')*)data-w-e-type=["']formula["']((?:[^<>"']+|"[^"]*"|'[^']*')*)>[\s\S]*?<\/span>/gi; return html.replace(formulaRegex, (match) => { const valueMatch = match.match(/data-value=["']([^"']+)["']/); if (!valueMatch) return match; let formula = valueMatch[1] .replace(/\\\\/g, '\\') .replace(/\\"/g, '"') .replace(/\\'/g, "'"); if (!formula.trim()) return ''; if (isSimpleFormula(formula)) { return simpleFormulaToHtml(formula); } const imageUrl = latexToImageUrl(formula); return ``; }); }; export const isHtmlContent = (value: string) => /<[a-z][\s\S]*>/i.test(value); export const formatQuestionHtml = (title?: string, options?: { stripImages?: boolean }) => { if (!title) return ''; let t = String(title); if (options?.stripImages) { t = t.replace(/]*formula-img)[^>]*\/?>/gi, ''); } t = t.replace(/<\/u>/gi, ''); t = t.replace(/http:\/\//gi, 'https://'); t = fixMathSymbolsInHtml(t); t = processFormula(t); return t; }; export const getQuestionStemSource = (item: Record) => { const pid = item.pidTitle ? String(item.pidTitle) : ''; const stem = String(item.stemHtml || item.titleMu || item.title || item.stem || ''); if (pid && stem) return `${pid}${stem}`; return pid || stem; }; export const getQuestionAnswerSource = (item: Record) => String(item.answerMu ?? item.answer ?? ''); const IMAGE_URL_PATTERN = /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i; export const isImageUrl = (url: string) => { const value = String(url || '').trim(); if (!value) return false; if (IMAGE_URL_PATTERN.test(value)) return true; return /(?:cos\.|myqcloud|oss-|\/temp\/|\/upload)/i.test(value); }; export const parseImageUrls = (raw: unknown) => { const urls = String(raw || '') .split(',') .map((item) => item.replace(/[`\s]/g, '').replace(/http:\/\//gi, 'https://').trim()) .filter(Boolean); return urls.filter(isImageUrl); }; export const extractAnswerImages = (item: Record) => { const images = new Set(); parseImageUrls(item.submitAnswerPic).forEach((url) => images.add(url)); const answer = String(item.submitAnswer || ''); const links = answer.match(/https?:\/\/[^\s,]+/gi) || []; links.forEach((link) => { const url = link.replace(/http:\/\//gi, 'https://').trim(); if (isImageUrl(url)) images.add(url); }); return Array.from(images); }; export const extractTitleImages = (html: string) => { if (!html) return [] as string[]; const images: string[] = []; const imgRegex = /]*\s*src=["']([^"']+)["'][^>]*\/?>/gi; let match; while ((match = imgRegex.exec(html)) !== null) { if (/formula-img/i.test(match[0])) continue; const url = (match[1] || '').trim().replace(/http:\/\//gi, 'https://'); if (url) images.push(url); } return images; };