lingxixue-mini-program/src/utils/questionHtml.ts
2026-05-29 20:39:12 +08:00

228 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 题目 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, '&lt;');
t = t.replace(/ > /g, ' &gt; ');
t = t.replace(/AUB/g, 'AB');
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;
};