228 lines
9.2 KiB
TypeScript
228 lines
9.2 KiB
TypeScript
/** 题目 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<string, string> = {
|
||
',': ',', '。': '.', ':': ':', ';': ';', '!': '!', '?': '?',
|
||
'(': '(', ')': ')', '【': '[', '】': ']', '{': '{', '}': '}',
|
||
'《': '<', '》': '>',
|
||
"\u201c": '"', "\u201d": '"', "\u2018": "'", "\u2019": "'",
|
||
'+': '+', '-': '-', '\u2212': '-', '×': '*', '÷': '/',
|
||
'=': '=', '<': '<', '>': '>', '%': '%', '&': '&',
|
||
'*': '*', '@': '@', '#': '#', '$': '$', '^': '^',
|
||
'~': '~', '|': '|', '\': '\\', '/': '/', ' ': ' ',
|
||
};
|
||
for (const [cn, en] of Object.entries(punctuationMap)) {
|
||
formula = formula.split(cn).join(en);
|
||
}
|
||
|
||
const mathSymbolMap: Record<string, string> = {
|
||
'∈': ' \\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<string, string> = {
|
||
'\\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<string, string> = {
|
||
'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
|
||
'5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
|
||
'+': '⁺', '-': '⁻', 'n': 'ⁿ',
|
||
};
|
||
const subscriptMap: Record<string, string> = {
|
||
'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 `<span class="formula-text" style="font-style:italic;">${text}</span>`;
|
||
};
|
||
|
||
export const processFormula = (html: string): string => {
|
||
if (html == null || typeof html !== 'string') return '';
|
||
if (!html) return '';
|
||
|
||
const formulaRegex = /<span\s+((?:[^<>"']+|"[^"]*"|'[^']*')*)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 `<img class="formula-img" src="${imageUrl}" style="height:1.4em;vertical-align:middle;" />`;
|
||
});
|
||
};
|
||
|
||
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(/<img(?![^>]*formula-img)[^>]*\/?>/gi, '');
|
||
}
|
||
t = t.replace(/<u><\/u>/gi, '<u class="fill-blank"></u>');
|
||
t = t.replace(/http:\/\//gi, 'https://');
|
||
t = fixMathSymbolsInHtml(t);
|
||
t = processFormula(t);
|
||
return t;
|
||
};
|
||
|
||
export const getQuestionStemSource = (item: Record<string, unknown>) => {
|
||
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, unknown>) =>
|
||
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<string, unknown>) => {
|
||
const images = new Set<string>();
|
||
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 = /<img[^>]*\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;
|
||
};
|