标签转换为标准 标签 $content = self::convertImageTags($content); // 1. 【关键修复】处理公式内的双反斜杠 -> 单反斜杠 // 数据库存储时 \sqrt 变成 \\sqrt,需要还原 $content = self::normalizeBackslashesInDelimiters($content); // 2. 如果内容中包含定界符,清理内部 HTML if (self::containsDelimiters($content)) { $content = self::cleanInsideDelimiters($content); } // 3. 检测内容类型:纯数学、混合内容还是纯文本 $contentType = self::detectContentType($content); // 4. 根据内容类型采取不同的处理策略 switch ($contentType) { case 'pure_math': // 纯数学表达式,如 "4x^2 - 25y^2" 或 "f(x) = x^2 - 4x + 5" return self::wrapPureMath($content); case 'mixed_content': // 混合内容,如 "已知函数 f(x) = x^2 - 4x + 5,求最小值" return self::smartWrapMixedContent($content); case 'delimited': // 已包含定界符的内容($...$, $$...$$, \(...\), \[...\]) // cleanInsideDelimiters() 已经清理了内部内容 // 渲染工作由客户端 KaTeX 或服务端 KatexRenderer 完成 // 【关键修复】将定界符内的 < > 编码为 HTML 实体,避免被浏览器当作 HTML 标签处理 return self::encodeAngleBracketsInDelimiters($content); case 'plain_text': default: // 纯文本,不需要处理 return $content; } } /** * 将自定义 标签转换为标准 标签 * 例如: => */ private static function convertImageTags(string $content): string { // 匹配 格式 return preg_replace( '/|><\/image>)/i', '', $content ); } /** * 【新增】将公式定界符内被JSON双重转义的LaTeX命令还原 * 例如:\\sqrt -> \sqrt, \\frac -> \frac * 但保留矩阵换行符 \\ (后面不跟字母的情况) */ private static function normalizeBackslashesInDelimiters(string $content): string { // 只替换 \\+小写字母 的情况(被JSON转义的LaTeX命令,如 \\sqrt -> \sqrt) // 保留 \\+大写字母 的情况(换行符后跟文本,如 \\CD 应保持为 \\CD) // 保留 \\+数字 或 \\+空白 的情况(矩阵换行符) $fixEscapedCommands = function ($tex) { // 保护多行环境中的换行符 \\,避免被误判为 LaTeX 命令 $placeholder = '__KATEX_BR__'; $originalTex = $tex; $protectedEnvs = []; $environments = [ 'cases', 'aligned', 'align', 'align*', 'array', 'matrix', 'pmatrix', 'bmatrix', 'vmatrix', 'Vmatrix', 'gather', 'split', 'eqnarray', ]; foreach ($environments as $env) { $pattern = '/\\\\begin\{' . preg_quote($env, '/') . '\}([\s\S]*?)\\\\end\{' . preg_quote($env, '/') . '\}/'; $tex = preg_replace_callback($pattern, function ($m) use ($env, $placeholder) { $content = str_replace('\\\\', $placeholder, $m[1]); return '\\begin{' . $env . '}' . $content . '\\end{' . $env . '}'; }, $tex); if ($tex !== $originalTex && !in_array($env, $protectedEnvs, true)) { $protectedEnvs[] = $env; } } // \\sqrt -> \sqrt, \\frac -> \frac, 但 \\CD 或 \\2 保持不变 // 【修复】只匹配小写字母,因为 LaTeX 命令都是小写 $tex = preg_replace('/\\\\\\\\([a-z])/', '\\\\$1', $tex); // 还原多行环境换行 $tex = str_replace($placeholder, '\\\\', $tex); if ($protectedEnvs) { Log::debug('MathFormulaProcessor: protected multiline line breaks', [ 'envs' => $protectedEnvs, ]); } return $tex; }; // 1. 处理 $$...$$ 块级公式 $content = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($matches) use ($fixEscapedCommands) { return '$$'.$fixEscapedCommands($matches[1]).'$$'; }, $content); // 2. 处理 $...$ 行内公式(避免与$$冲突) $content = preg_replace_callback('/(? 编码为 HTML 实体 * 避免 LaTeX 公式中的 < > 被浏览器当作 HTML 标签处理 * 例如:$x<4$ 中的 <4 会被浏览器当作无效标签移除 */ private static function encodeAngleBracketsInDelimiters(string $content): string { $encodeInner = function (string $tex): string { // 将 < 和 > 编码为 HTML 实体,KaTeX 会正确处理这些实体 return str_replace(['<', '>'], ['<', '>'], $tex); }; // 1. 处理 $$...$$ 块级公式 $content = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($matches) use ($encodeInner) { return '$$' . $encodeInner($matches[1]) . '$$'; }, $content); // 2. 处理 $...$ 行内公式(避免与$$冲突) $content = preg_replace_callback('/(? 避免被浏览器当作 HTML 标签 $encoded = str_replace(['<', '>'], ['<', '>'], $content); return '$' . $encoded . '$'; } /** * 清理定界符内部的 HTML 标签 * 【修复】不再使用 strip_tags(),因为它会把 LaTeX 中的 < > 当作标签删除 * 例如:$x<4$ 中的 <4 会被 strip_tags 误删 */ private static function cleanInsideDelimiters(string $content): string { // 修复:使用更精确的正则表达式,避免模式冲突 // 只移除真正的 HTML 标签(如 ,
, 等),保留数学符号 < > // 定义安全的 HTML 标签清理函数(只移除真正的 HTML 标签) $cleanHtmlTags = function (string $tex): string { // 只移除看起来像 HTML 标签的内容(以字母开头的标签) // 例如:, ,
, 但保留 <4, >0, x]*>/', '', $tex); // 解码 HTML 实体 $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8'); return trim($tex); }; // 1. 处理 $$...$$ 显示公式 $content = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($matches) use ($cleanHtmlTags) { return '$$' . $cleanHtmlTags($matches[1]) . '$$'; }, $content); // 2. 处理 \(...\) 行内公式 $content = preg_replace_callback('/\\\\\(([\s\S]*?)\\\\\)/', function ($matches) use ($cleanHtmlTags) { return '\\(' . $cleanHtmlTags($matches[1]) . '\\)'; }, $content); // 3. 处理 \[...\] 显示公式 $content = preg_replace_callback('/\\\\\[([\s\S]*?)\\\\\]/', function ($matches) use ($cleanHtmlTags) { return '\\[' . $cleanHtmlTags($matches[1]) . '\\]'; }, $content); // 4. 最后处理 $...$ 行内公式(避免与$$冲突) $content = preg_replace_callback('/(?]+>'; $existingDelimiterPattern = '(?:\$\$[\s\S]*?\$\$|\$[\s\S]*?\$|\\\\\([\s\S]*?\\\\\)|\\\\\[[\s\S]*?\\\\\])'; // 数学公式模式(按优先级排列) $patterns = [ // 1. 函数定义: f(x) = 2x^3 - 3x^2 + 4x - 5 "[a-zA-Z]'?\\([a-zA-Z0-9,\\s]+\\)\\s*=\\s*[a-zA-Z0-9\\+\\-\\*\\/\\^\\s\\.\\(\\)\\_\\{\\}]+", // 2. 导数/函数调用: f'(1), g(5), sin(x) "[a-zA-Z]+'?\\([a-zA-Z0-9\\+\\-\\*\\/\\^\\s\\.]+\\)", // 3. LaTeX 命令: \frac{1}{2} '\\\\[a-zA-Z]+\\{[^}]*\\}(?:\\{[^}]*\\})?', // 4. 数学表达式: x^2 + y^2, 2x - 3 '[a-zA-Z0-9]+[\\^_][a-zA-Z0-9\\{\\}]+(?:\\s*[\\+\\-\\*\\/]\\s*[a-zA-Z0-9\\^_\\{\\}\\.]+)*', ]; $mathPattern = '(?:'.implode('|', $patterns).')'; $pattern = "/($tagPattern)|($existingDelimiterPattern)|($mathPattern)/u"; return preg_replace_callback($pattern, function ($matches) { // HTML 标签,原样返回 if (! empty($matches[1])) { return $matches[1]; } // 已有的定界符,原样返回 if (! empty($matches[2])) { return $matches[2]; } // 数学公式,添加 $ 包裹 if (! empty($matches[3])) { $math = trim($matches[3]); // 再次检查是否已经包裹 if (str_contains($math, '$')) { return $math; } // 【修复】编码 < > 避免被浏览器当作 HTML 标签 $encoded = str_replace(['<', '>'], ['<', '>'], $math); return '$' . $encoded . '$'; } return $matches[0]; }, $content); } /** * 检查是否已有定界符 */ private static function hasDelimiters(string $content): bool { $content = trim($content); // 检查 $$...$$ if (str_starts_with($content, '$$') && str_ends_with($content, '$$')) { return true; } // 检查 $...$ if (str_starts_with($content, '$') && str_ends_with($content, '$')) { return true; } // 检查 \[...\] if (str_starts_with($content, '\\[') && str_ends_with($content, '\\]')) { return true; } // 检查 \(...\) if (str_starts_with($content, '\\(') && str_ends_with($content, '\\)')) { return true; } return false; } /** * 检测数学特征 * 优化:更精确的检测,减少误判 */ private static function containsMathFeatures(string $content): bool { // 1. 检查是否有 LaTeX 命令 if (preg_match('/\\\\[a-zA-Z]+\{?/', $content)) { return true; } // 2. 检查函数定义或等式(如 f(x) =, g(x) =) // 必须是:字母+括号+等号+数学内容 if (preg_match('/[a-zA-Z]\([a-zA-Z0-9,\s]+\)\s*=\s*[a-zA-Z0-9\+\-\*\/\^\.\(\)\s\\\\_\{]+/', $content)) { return true; } // 3. 检查纯数学表达式(只包含数字、变量、运算符、括号) // 严格的数学表达式:必须包含字母和运算符,且没有中文字符 if (preg_match('/^[a-zA-Z0-9\+\-\*\/\=\s\.\^\(\)\_\{\}]+$/', $content) && preg_match('/[a-zA-Z]/', $content) && preg_match('/[\+\-\*\/\=\^]/', $content)) { return true; } // 4. 检查包含变量的数学表达式(带约束) // 必须有明确的运算符连接,且周围是数学内容 if (preg_match('/[a-zA-Z0-9\.\^\_\{\}]\s*[\+\-\*\/]\s*[a-zA-Z0-9\.\^\_\{\}\(\)]/', $content)) { return true; } // 5. 检查分数形式(如 \frac{}{}) if (preg_match('/\\\\frac\{/', $content)) { return true; } // 6. 检查上标或下标(仅当与数字/字母组合时) if (preg_match('/[a-zA-Z0-9]\s*[\^_]\s*[a-zA-Z0-9]/', $content)) { return true; } return false; } /** * 批量处理 */ public static function processArray(array $data, array $fieldsToProcess): array { foreach ($data as $key => &$value) { if (in_array($key, $fieldsToProcess)) { if (is_string($value)) { $value = self::processFormulas($value); } elseif (is_array($value)) { // 【修复】当字段在处理列表中且值是数组时(如 options),处理数组中的每个字符串元素 $value = self::processArrayValues($value); } } elseif (is_array($value)) { $value = self::processArray($value, $fieldsToProcess); } } return $data; } /** * 【新增】递归处理数组中的所有字符串值 * 用于处理 options 等数组类型的字段 */ private static function processArrayValues(array $arr): array { foreach ($arr as $key => &$value) { if (is_string($value)) { $value = self::processFormulas($value); } elseif (is_array($value)) { $value = self::processArrayValues($value); } } return $arr; } /** * 处理题目数据 */ public static function processQuestionData(array $question): array { $fieldsToProcess = [ 'stem', 'content', 'question_text', 'answer', 'correct_answer', 'student_answer', 'explanation', 'solution', 'question_content', 'options', ]; return self::processArray($question, $fieldsToProcess); } /** * 修复被污染的数学公式(包含重复的转义字符) */ private static function fixCorruptedFormulas(string $content): string { // 简化的修复策略,只处理明确的问题 // 1. 将超过2个连续的$符号减少为2个 $content = preg_replace('/\${3,}/', '$$', $content); // 2. 修复$$B . - \frac{1}{2}$$ 这种格式,在选项前加空格 $content = preg_replace('/\$\$([A-Z])\s*\.\s*/', '$$ $1. ', $content); // 3. 修复不完整的frac命令:\frac{1}{2} -> \frac{1}{2} $content = preg_replace('/\\\\frac\\\\({[^}]+)([^}]*)\\\\/', '\\\\frac$1}{$2}', $content); // 4. 移除孤立的反斜杠(在非LaTeX命令前的) $content = preg_replace('/\\\\(?![a-zA-Z{])/', '', $content); return $content; } }