标签转换为标准
标签
$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 完成
return $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('/(?]+>';
$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;
}
return '$'.$math.'$';
}
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;
}
}