Răsfoiți Sursa

fix(pdf): unify blank placeholder rendering across choice/fill and cover math tail markers

yemeishu 3 săptămâni în urmă
părinte
comite
ba1b65576e

+ 102 - 0
app/Support/BlankPlaceholderRenderer.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Support;
+
+class BlankPlaceholderRenderer
+{
+    private const DEFAULT_BLANK_SPAN = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
+
+    /**
+     * 将题干中的空括号/下划线/部分异常占位符统一替换为标准空位样式。
+     *
+     * @return array{0:string,1:bool} [renderedContent, replacedAnyPlaceholder]
+     */
+    public static function replaceToBlankSpan(string $content, ?string $blankSpan = null, bool $collapseAdjacentBlanks = false): array
+    {
+        $blankSpan = $blankSpan ?: self::DEFAULT_BLANK_SPAN;
+        $renderedContent = $content;
+
+        $latexPlaceholders = [];
+        $counter = 0;
+        $renderedContent = preg_replace_callback('/\$(?:[^\$]|\\\\.)*\$/u', function ($matches) use (&$latexPlaceholders, &$counter, $blankSpan) {
+            $latexContent = $matches[0];
+            $inner = mb_substr($latexContent, 1, mb_strlen($latexContent) - 2);
+
+            // 数学环境内也可能包含填空占位符(如 $\\underline{\\qquad}$ / $\\angle A=\\underline{\\quad}$)
+            $blankToken = '<<<BLANK_IN_MATH_'.$counter.'>>>';
+            $innerWithBlanks = preg_replace(
+                [
+                    '/\\\\underline\{[^}]*\}/u',
+                    '/\\\\qquad+/u',
+                    '/\\\\quad+/u',
+                    '/[((](?:\s|&nbsp;|&#160;| )*[))]/u',
+                    '/_{2,}/u',
+                ],
+                $blankToken,
+                $inner,
+                -1,
+                $blankCount
+            );
+            if ($blankCount > 0) {
+                $parts = explode($blankToken, $innerWithBlanks);
+                $rebuilt = '';
+                $lastIndex = count($parts) - 1;
+                foreach ($parts as $index => $part) {
+                    if ($part !== '') {
+                        $rebuilt .= htmlspecialchars('$'.$part.'$', ENT_QUOTES | ENT_HTML5, 'UTF-8');
+                    }
+                    if ($index < $lastIndex) {
+                        $rebuilt .= $blankSpan;
+                    }
+                }
+
+                return $rebuilt === '' ? $blankSpan : $rebuilt;
+            }
+
+            $placeholder = '<<<LATEX_BLANK_'.$counter.'>>>';
+            $latexPlaceholders[$placeholder] = $latexContent;
+            $counter++;
+
+            return $placeholder;
+        }, $renderedContent);
+
+        // 兼容常见空位写法:\underline{...}、\qquad、空括号(含 nbsp 等空白)、连续下划线、尾部 \\$
+        $patterns = [
+            '/\\\underline\{[^}]*\}/u',
+            '/\\\qquad+/u',
+            '/[((](?:\s|&nbsp;|&#160;| )*[))]/u',
+            '/_{2,}/u',
+            '/\\\\+\$(?=\s*$)/u',
+        ];
+        $renderedContent = preg_replace($patterns, $blankSpan, $renderedContent);
+        if ($collapseAdjacentBlanks) {
+            $quotedBlankSpan = preg_quote($blankSpan, '/');
+            $renderedContent = preg_replace('/(?:'.$quotedBlankSpan.'(?:\s|&nbsp;|&#160;| )*){2,}/u', $blankSpan, $renderedContent);
+        }
+
+        foreach ($latexPlaceholders as $placeholder => $latexContent) {
+            if (preg_match('/^\$(.*?)(\\\\+)\$$/u', $latexContent, $match)) {
+                $inner = rtrim($match[1]);
+                if ($inner === '' || preg_match('/[=::]\s*$/u', $inner)) {
+                    if ($inner === '') {
+                        $replacement = $blankSpan;
+                    } else {
+                        $replacement = htmlspecialchars('$'.$inner.'$', ENT_QUOTES | ENT_HTML5, 'UTF-8').' '.$blankSpan;
+                    }
+                    $renderedContent = str_replace($placeholder, $replacement, $renderedContent);
+                    continue;
+                }
+            }
+
+            $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+            $renderedContent = str_replace($placeholder, $encodedLatex, $renderedContent);
+        }
+
+        return [$renderedContent, $renderedContent !== $content];
+    }
+
+    public static function defaultBlankSpan(): string
+    {
+        return self::DEFAULT_BLANK_SPAN;
+    }
+}

+ 1 - 25
app/Support/GradingStyleQuestionStem.php

@@ -122,31 +122,7 @@ class GradingStyleQuestionStem
      */
     private static function applyBlankPlaceholdersLikeGrading(string $stemLine): string
     {
-        $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
-        $renderedStem = $stemLine;
-        $renderedStem = preg_replace('/\\\underline\{[^}]*\}/', $blankSpan, $renderedStem);
-        $renderedStem = preg_replace('/\\\qquad+/', $blankSpan, $renderedStem);
-
-        $latexPlaceholders = [];
-        $counter = 0;
-        $renderedStem = preg_replace_callback('/\$[^$]+\$/u', function ($matches) use (&$latexPlaceholders, &$counter) {
-            $placeholder = '<<<LATEX_'.$counter.'>>>';
-            $latexPlaceholders[$placeholder] = $matches[0];
-            $counter++;
-
-            return $placeholder;
-        }, $renderedStem);
-
-        $renderedStem = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedStem);
-
-        foreach ($latexPlaceholders as $placeholder => $latexContent) {
-            $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
-            $renderedStem = str_replace($placeholder, $encodedLatex, $renderedStem);
-        }
-
-        if ($renderedStem === $stemLine) {
-            $renderedStem .= ' '.$blankSpan;
-        }
+        [$renderedStem] = BlankPlaceholderRenderer::replaceToBlankSpan($stemLine, null, true);
 
         return $renderedStem;
     }

+ 6 - 57
resources/views/components/exam/paper-body.blade.php

@@ -149,37 +149,8 @@
                     $stemLine = trim($stemMatch[1]);
                 }
             }
-            // 将题干中的空括号/下划线替换为短波浪线;如无占位符,则在末尾追加短波浪线
-            $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
-            // 【修复】扩展下划线转换规则,支持LaTeX格式和多种占位符
-            $renderedStem = $stemLine;
-            // 先处理LaTeX格式的underline命令
-            $renderedStem = preg_replace('/\\\underline\{[^}]*\}/', $blankSpan, $renderedStem);
-            $renderedStem = preg_replace('/\\\qquad+/', $blankSpan, $renderedStem);
-            // 【修复】在处理填空占位符时,保护LaTeX公式不被破坏
-            // 先标记LaTeX公式区域
-            $latexPlaceholders = [];
-            $counter = 0;
-            $renderedStem = preg_replace_callback('/\$[^$]+\$/u', function($matches) use (&$latexPlaceholders, &$counter, $blankSpan) {
-                $placeholder = '<<<LATEX_' . $counter . '>>>';
-                $latexPlaceholders[$placeholder] = $matches[0];
-                $counter++;
-                return $placeholder;
-            }, $renderedStem);
-
-            // 现在处理普通占位符(不会破坏LaTeX公式)
-            $renderedStem = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedStem);
-
-            // 恢复LaTeX公式(并进行HTML实体编码防止被浏览器解析)
-            foreach ($latexPlaceholders as $placeholder => $latexContent) {
-                $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
-                $renderedStem = str_replace($placeholder, $encodedLatex, $renderedStem);
-            }
-
-            // 如果没有占位符,在末尾添加
-            if ($renderedStem === $stemLine) {
-                $renderedStem .= ' ' . $blankSpan;
-            }
+            // 选择题只做占位符归一,不再兜底追加下划线,避免出现“( )+下划线”重复。
+            [$renderedStem] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan($stemLine, null, true);
             $renderedStem = $mathProcessed ? $renderedStem : \App\Services\MathFormulaProcessor::processFormulas($renderedStem);
         @endphp
         <div class="question">
@@ -312,32 +283,10 @@
         @php
             // 【修复】使用question_number字段作为显示序号,确保全局序号一致性
             $questionNumber = $q->question_number ?? (count($choiceQuestions) + $index + 1);
-            $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
-            // 【修复】扩展下划线转换规则,支持LaTeX格式和多种占位符
-            $renderedContent = $q->content;
-            // 【修复】在处理填空占位符时,保护LaTeX公式不被破坏
-            // 先标记LaTeX公式区域(支持包含反斜杠和花括号的LaTeX命令)
-            $latexPlaceholders = [];
-            $counter = 0;
-            $renderedContent = preg_replace_callback('/\$(?:[^\$]|\\.)*\$/u', function($matches) use (&$latexPlaceholders, &$counter, $blankSpan) {
-                $placeholder = '<<<LATEX_FILL_' . $counter . '>>>';
-                $latexPlaceholders[$placeholder] = $matches[0];
-                $counter++;
-                return $placeholder;
-            }, $renderedContent);
-
-            // 现在处理普通占位符(不会破坏LaTeX公式)
-            $renderedContent = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedContent);
-
-            // 恢复LaTeX公式(并进行HTML实体编码防止被浏览器解析)
-            foreach ($latexPlaceholders as $placeholder => $latexContent) {
-                $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
-                $renderedContent = str_replace($placeholder, $encodedLatex, $renderedContent);
-            }
-
-            // 如果没有占位符且内容没有变化,在末尾添加
-            // 但要检查是否已经有填空占位符(如\underline{\qquad})
-            if ($renderedContent === $q->content && !preg_match('/\\\\underline|\\\\qquad|(\s*)|\(\s*\)/', $renderedContent)) {
+            $blankSpan = \App\Support\BlankPlaceholderRenderer::defaultBlankSpan();
+            [$renderedContent, $hasPlaceholders] = \App\Support\BlankPlaceholderRenderer::replaceToBlankSpan((string) $q->content, $blankSpan, false);
+            // 填空题保留兜底:题干无任何占位时,在末尾补一个标准空位。
+            if (!$hasPlaceholders) {
                 $renderedContent .= ' ' . $blankSpan;
             }
             $renderedContent = $mathProcessed ? $renderedContent : \App\Services\MathFormulaProcessor::processFormulas($renderedContent);