GradingStyleQuestionStem.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. <?php
  2. namespace App\Support;
  3. use App\Services\MathFormulaProcessor;
  4. /**
  5. * 与判卷页 components/exam/paper-body 选择题题干处理口径一致:
  6. * 去题号前缀、separateStemAndOptions 分离选项、填空占位虚线、MathFormulaProcessor::processFormulas
  7. */
  8. class GradingStyleQuestionStem
  9. {
  10. /**
  11. * 选项字母映射由下方 separateStemAndOptions / extractOptions 从正文解析;与判卷页一致。
  12. */
  13. public static function buildChoiceStemForReport(string $rawQuestionText): string
  14. {
  15. $html = is_string($rawQuestionText) ? $rawQuestionText : '';
  16. $cleanContent = preg_replace('/^\d+[\.、]\s*/', '', trim($html));
  17. [$stemLine] = self::separateStemAndOptions($cleanContent);
  18. $renderedStem = self::applyBlankPlaceholdersLikeGrading($stemLine);
  19. return MathFormulaProcessor::processFormulas($renderedStem);
  20. }
  21. /**
  22. * 与 ExamPdfController::separateStemAndOptions 一致
  23. *
  24. * @return array{0: string, 1: array<int, string>}
  25. */
  26. public static function separateStemAndOptions(string $content): array
  27. {
  28. $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
  29. $hasOptions = preg_match('/(?:^|\s)[A-D][\.、:.:]/u', $contentWithoutSvg);
  30. if (! $hasOptions) {
  31. return [$content, []];
  32. }
  33. $options = self::extractOptions($content);
  34. if (! empty($options)) {
  35. if (preg_match('/^(.+?)(?=(?:^|\s)[A-D][\.、:.:])/su', $contentWithoutSvg, $match)) {
  36. $stem = trim($match[1]);
  37. if (strpos($stem, '[SVG_PLACEHOLDER]') !== false) {
  38. foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
  39. if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
  40. $pos = $m[0][1];
  41. if ($pos > 0) {
  42. $stem = trim(mb_substr($content, 0, $pos));
  43. break;
  44. }
  45. }
  46. }
  47. }
  48. } else {
  49. $stem = $content;
  50. foreach (['A.', 'A、', 'A:', 'A.', 'A:'] as $marker) {
  51. if (preg_match('/\s'.preg_quote($marker, '/').'/', $content, $m, PREG_OFFSET_CAPTURE)) {
  52. $pos = $m[0][1];
  53. if ($pos > 0) {
  54. $stem = trim(mb_substr($content, 0, $pos));
  55. break;
  56. }
  57. }
  58. }
  59. }
  60. $stem = preg_replace('/()\s*$/', '', $stem);
  61. $stem = trim($stem);
  62. return [$stem, $options];
  63. }
  64. return [$content, []];
  65. }
  66. /**
  67. * 与 ExamPdfController::extractOptions 一致
  68. *
  69. * @return array<int, string>
  70. */
  71. public static function extractOptions(string $content): array
  72. {
  73. $options = [];
  74. $contentWithoutSvg = preg_replace('/<svg[^>]*>.*?<\/svg>/is', '[SVG_PLACEHOLDER]', $content);
  75. $pattern = '/(?:^|\s)([A-D])[\.、:.:]\s*(.+?)(?=(?:^|\s)[A-D][\.、:.:]|$)/su';
  76. if (preg_match_all($pattern, $contentWithoutSvg, $matches, PREG_SET_ORDER)) {
  77. foreach ($matches as $match) {
  78. $optionText = trim($match[2]);
  79. $optionText = preg_replace('/\s+$/', '', $optionText);
  80. $optionText = preg_replace('/^\$\$\s*/', '', $optionText);
  81. $optionText = preg_replace('/\s*\$\$$/', '', $optionText);
  82. if (! empty($optionText)) {
  83. $options[] = $optionText;
  84. }
  85. }
  86. }
  87. if (empty($options)) {
  88. $lines = preg_split('/[\r\n]+/', $contentWithoutSvg);
  89. foreach ($lines as $line) {
  90. $line = trim($line);
  91. if (preg_match('/^([A-D])[\.、:.:]\s*(.+)$/u', $line, $match)) {
  92. $optionText = trim($match[2]);
  93. if (! empty($optionText)) {
  94. $options[] = $optionText;
  95. }
  96. }
  97. }
  98. }
  99. return $options;
  100. }
  101. /**
  102. * 与 paper-body 选择题中 $renderedStem 逻辑一致
  103. */
  104. private static function applyBlankPlaceholdersLikeGrading(string $stemLine): string
  105. {
  106. $blankSpan = '<span style="display:inline-block; min-width:80px; border-bottom:1.2px dashed #444; vertical-align:bottom;">&nbsp;</span>';
  107. $renderedStem = $stemLine;
  108. $renderedStem = preg_replace('/\\\underline\{[^}]*\}/', $blankSpan, $renderedStem);
  109. $renderedStem = preg_replace('/\\\qquad+/', $blankSpan, $renderedStem);
  110. $latexPlaceholders = [];
  111. $counter = 0;
  112. $renderedStem = preg_replace_callback('/\$[^$]+\$/u', function ($matches) use (&$latexPlaceholders, &$counter) {
  113. $placeholder = '<<<LATEX_'.$counter.'>>>';
  114. $latexPlaceholders[$placeholder] = $matches[0];
  115. $counter++;
  116. return $placeholder;
  117. }, $renderedStem);
  118. $renderedStem = preg_replace(['/(\s*)/u', '/\(\s*\)/', '/_{2,}/'], $blankSpan, $renderedStem);
  119. foreach ($latexPlaceholders as $placeholder => $latexContent) {
  120. $encodedLatex = htmlspecialchars($latexContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
  121. $renderedStem = str_replace($placeholder, $encodedLatex, $renderedStem);
  122. }
  123. if ($renderedStem === $stemLine) {
  124. $renderedStem .= ' '.$blankSpan;
  125. }
  126. return $renderedStem;
  127. }
  128. }