Browse Source

Harden knowledge explanation markdown rendering

yemeishu 2 weeks ago
parent
commit
b1bd2d7235
1 changed files with 113 additions and 19 deletions
  1. 113 19
      app/Services/ExamPdfExportService.php

+ 113 - 19
app/Services/ExamPdfExportService.php

@@ -1868,19 +1868,11 @@ class ExamPdfExportService
 
 
     private function renderKpExplainMarkdown(string $html): string
     private function renderKpExplainMarkdown(string $html): string
     {
     {
-        if (! class_exists(\Michelf\MarkdownExtra::class)) {
-            return $html;
-        }
-
-        $parser = new \Michelf\MarkdownExtra;
-
         return preg_replace_callback(
         return preg_replace_callback(
             '/<div class="kp-markdown-source"[^>]*>([\s\S]*?)<\/div>\s*<div class="kp-markdown-container[^"]*"[^>]*><\/div>/i',
             '/<div class="kp-markdown-source"[^>]*>([\s\S]*?)<\/div>\s*<div class="kp-markdown-container[^"]*"[^>]*><\/div>/i',
-            function ($matches) use ($parser) {
+            function ($matches) {
                 $markdown = html_entity_decode(trim($matches[1]), ENT_QUOTES, 'UTF-8');
                 $markdown = html_entity_decode(trim($matches[1]), ENT_QUOTES, 'UTF-8');
-                [$protectedMarkdown, $mathPlaceholders] = $this->protectLatexBlocksForMarkdown($markdown);
-                $rendered = $parser->transform($protectedMarkdown);
-                $rendered = strtr($rendered, $mathPlaceholders);
+                $rendered = $this->renderKpMarkdownContent($markdown);
 
 
                 return '<div class="kp-markdown-container kp-markdown-content">'.$rendered.'</div>';
                 return '<div class="kp-markdown-container kp-markdown-content">'.$rendered.'</div>';
             },
             },
@@ -5047,24 +5039,116 @@ MARKDOWN;
             return '';
             return '';
         }
         }
 
 
-        if ($this->looksLikeHtml($content)) {
+        if ($this->looksLikeRenderedKpHtml($content)) {
             return $content;
             return $content;
         }
         }
 
 
-        if (! class_exists(\Michelf\MarkdownExtra::class)) {
-            $safe = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
-            return '<div class="kp-markdown-container kp-markdown-content">'.nl2br($safe).'</div>';
+        if ($this->looksLikeHtml($content) && ! $this->looksLikeMarkdown($content)) {
+            return $content;
         }
         }
 
 
-        $parser = new \Michelf\MarkdownExtra;
         $markdown = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
         $markdown = html_entity_decode($content, ENT_QUOTES, 'UTF-8');
-        [$protectedMarkdown, $mathPlaceholders] = $this->protectLatexBlocksForMarkdown($markdown);
-        $rendered = $parser->transform($protectedMarkdown);
-        $rendered = strtr($rendered, $mathPlaceholders);
+        $rendered = $this->renderKpMarkdownContent($markdown);
 
 
         return '<div class="kp-markdown-container kp-markdown-content">'.$rendered.'</div>';
         return '<div class="kp-markdown-container kp-markdown-content">'.$rendered.'</div>';
     }
     }
 
 
+    private function renderKpMarkdownContent(string $markdown): string
+    {
+        if (class_exists(\Michelf\MarkdownExtra::class)) {
+            $parser = new \Michelf\MarkdownExtra;
+            [$protectedMarkdown, $mathPlaceholders] = $this->protectLatexBlocksForMarkdown($markdown);
+            $rendered = $parser->transform($protectedMarkdown);
+
+            return strtr($rendered, $mathPlaceholders);
+        }
+
+        return $this->renderBasicKpMarkdown($markdown);
+    }
+
+    private function renderBasicKpMarkdown(string $markdown): string
+    {
+        $lines = preg_split('/\R/u', trim($markdown));
+        if ($lines === false) {
+            $safe = htmlspecialchars($markdown, ENT_QUOTES, 'UTF-8');
+
+            return nl2br($safe);
+        }
+
+        $html = [];
+        $paragraph = [];
+        $listType = null;
+
+        $flushParagraph = function () use (&$html, &$paragraph): void {
+            if ($paragraph === []) {
+                return;
+            }
+
+            $html[] = '<p>'.implode('<br>', $paragraph).'</p>';
+            $paragraph = [];
+        };
+        $closeList = function () use (&$html, &$listType): void {
+            if ($listType === null) {
+                return;
+            }
+
+            $html[] = "</{$listType}>";
+            $listType = null;
+        };
+        $openList = function (string $type) use (&$html, &$listType, $closeList): void {
+            if ($listType === $type) {
+                return;
+            }
+
+            $closeList();
+            $html[] = "<{$type}>";
+            $listType = $type;
+        };
+
+        foreach ($lines as $line) {
+            $line = rtrim($line);
+            if ($line === '') {
+                $flushParagraph();
+                $closeList();
+                continue;
+            }
+
+            if (preg_match('/^(#{1,6})\s+(.+)$/u', $line, $match)) {
+                $flushParagraph();
+                $closeList();
+                $level = min(6, strlen($match[1]));
+                $html[] = sprintf('<h%d>%s</h%d>', $level, $this->escapeMarkdownLine($match[2]), $level);
+                continue;
+            }
+
+            if (preg_match('/^\s*[-*]\s+(.+)$/u', $line, $match)) {
+                $flushParagraph();
+                $openList('ul');
+                $html[] = '<li>'.$this->escapeMarkdownLine($match[1]).'</li>';
+                continue;
+            }
+
+            if (preg_match('/^\s*\d+\.\s+(.+)$/u', $line, $match)) {
+                $flushParagraph();
+                $openList('ol');
+                $html[] = '<li>'.$this->escapeMarkdownLine($match[1]).'</li>';
+                continue;
+            }
+
+            $paragraph[] = $this->escapeMarkdownLine($line);
+        }
+
+        $flushParagraph();
+        $closeList();
+
+        return implode("\n", $html);
+    }
+
+    private function escapeMarkdownLine(string $line): string
+    {
+        return htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
+    }
+
     /**
     /**
      * 保护 Markdown 中的数学块,避免 MarkdownExtra 吃掉 LaTeX 反斜杠
      * 保护 Markdown 中的数学块,避免 MarkdownExtra 吃掉 LaTeX 反斜杠
      */
      */
@@ -5086,16 +5170,26 @@ MARKDOWN;
         return [$protected ?? $markdown, $placeholders];
         return [$protected ?? $markdown, $placeholders];
     }
     }
 
 
-    private function looksLikeHtml(string $content): bool
+    private function looksLikeRenderedKpHtml(string $content): bool
     {
     {
         if (stripos($content, 'kp-markdown-container') !== false ||
         if (stripos($content, 'kp-markdown-container') !== false ||
             stripos($content, 'kp-markdown-content') !== false) {
             stripos($content, 'kp-markdown-content') !== false) {
             return true;
             return true;
         }
         }
 
 
+        return false;
+    }
+
+    private function looksLikeHtml(string $content): bool
+    {
         return (bool) preg_match('/<\s*(p|div|h[1-6]|ul|ol|li|table|span|blockquote|pre|code|br)\b/i', $content);
         return (bool) preg_match('/<\s*(p|div|h[1-6]|ul|ol|li|table|span|blockquote|pre|code|br)\b/i', $content);
     }
     }
 
 
+    private function looksLikeMarkdown(string $content): bool
+    {
+        return (bool) preg_match('/(^|\R)\s{0,3}#{1,6}\s+\S|(^|\R)\s{0,3}[-*]\s+\S|(^|\R)\s{0,3}\d+\.\s+\S/u', $content);
+    }
+
     private function shouldUseDefaultExplanations(): bool
     private function shouldUseDefaultExplanations(): bool
     {
     {
         if (!Schema::hasTable('knowledge_points')) {
         if (!Schema::hasTable('knowledge_points')) {