/** 题目 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;
};