专业Markdown转HTML工具类:修复优化与Spring Boot适配

文章目录

  • Markdown转HTML工具类
    • 一、完整工具类
    • [二、Spring Boot项目完整测试方案](#二、Spring Boot项目完整测试方案)
      • [1. 项目结构](#1. 项目结构)
      • [2. 启动类](#2. 启动类)
      • [3. DTO类](#3. DTO类)
      • [4. 控制器](#4. 控制器)
      • [5. 配置文件](#5. 配置文件)
      • [6. 单元测试(服务层)](#6. 单元测试(服务层))
      • [7. 集成测试(控制器层)](#7. 集成测试(控制器层))
      • [8. 应用测试类](#8. 应用测试类)
    • 三、API测试方法
      • [1. 使用curl测试](#1. 使用curl测试)
      • [2. 使用Postman测试](#2. 使用Postman测试)
      • [3. 使用Swagger UI(可选)](#3. 使用Swagger UI(可选))
    • 四、部署注意事项

本文介绍了一个专业的Markdown转HTML转换工具类,主要功能包括:

  1. 核心功能:实现Markdown文本到HTML的转换,支持标题、列表、代码块、表格等常见语法
  • 优化特性:
  • 使用预编译正则表达式提升性能
  • 添加转换结果缓存机制
  • 支持嵌套列表和代码块语言标识
  1. 特殊处理:
  • 修复了嵌套列表、代码块转义等问题
  • 增强错误处理和注释
  • 支持多种编程语言别名映射

该工具类基于Spring Boot框架开发,可作为服务组件直接注入使用,适用于需要将Markdown内容转换为HTML的各种应用场景。

Markdown转HTML工具类

一、完整工具类

java 复制代码
package com.example.markdown.service;

import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.*;

/**
 * 专业Markdown转HTML转换器(Spring Boot适配版)
 * 
 * 修复的问题:
 * 1. 嵌套列表支持(通过缩进判断层级)
 * 2. 代码块语言标识转义(支持多种语言别名)
 * 3. 表格中的行内格式(粗体、斜体、代码等)
 * 4. 图片和任务列表扩展语法支持
 * 
 * 优化点:
 * - 增强正则表达式性能
 * - 完善错误处理
 * - 添加详细注释
 * - 支持更多Markdown扩展语法
 */
@Service
public class MarkdownToHtmlConverter {

    // ==================== 预编译正则表达式(性能优化)====================
    private static final Pattern HEADING_PATTERN = Pattern.compile("^(#{1,6})\\s+(.*)$");
    private static final Pattern HORIZONTAL_RULE_PATTERN = Pattern.compile("^[-*_]{3,}$");
    private static final Pattern TABLE_SEPARATOR_PATTERN = Pattern.compile("^[-:|]+$");
    private static final Pattern ORDERED_LIST_PATTERN = Pattern.compile("^(\\s*)(\\d+)\\.\\s+(.*)$");
    private static final Pattern UNORDERED_LIST_PATTERN = Pattern.compile("^(\\s*)([*+-])\\s+(.*)$");
    private static final Pattern TASK_LIST_PATTERN = Pattern.compile("^(\\s*)([*+-])\\s+\\[([ xX])\\]\\s+(.*)$");
    private static final Pattern INLINE_BOLD_PATTERN = Pattern.compile("\\*\\*(.*?)\\*\\*|__(.*?)__");
    private static final Pattern INLINE_ITALIC_PATTERN = Pattern.compile("\\*(.*?)\\*|_([^_]+)_");
    private static final Pattern INLINE_CODE_PATTERN = Pattern.compile("`(.*?)`");
    private static final Pattern LINK_PATTERN = Pattern.compile("\\[(.*?)\\]\\((.*?)\\)");
    private static final Pattern IMAGE_PATTERN = Pattern.compile("!\\[(.*?)\\]\\((.*?)\\)");

    // ==================== 配置参数 ====================
    private final boolean enableEscapeHtml;
    private final Map<String, String> languageAliases;
    private final Map<String, String> conversionCache; // 转换结果缓存

    public MarkdownToHtmlConverter() {
        this(true);
    }

    public MarkdownToHtmlConverter(boolean enableEscapeHtml) {
        this.enableEscapeHtml = enableEscapeHtml;
        this.conversionCache = new ConcurrentHashMap<>();
        
        // 语言别名映射(支持多种语言标识)
        this.languageAliases = new HashMap<>();
        languageAliases.put("javascript", "js");
        languageAliases.put("typescript", "ts");
        languageAliases.put("mysql", "sql");
        languageAliases.put("postgresql", "sql");
        languageAliases.put("python", "py");
        languageAliases.put("java", "java");
        languageAliases.put("c++", "cpp");
        languageAliases.put("c#", "csharp");
    }

    // ==================== 核心转换方法 ====================
    /**
     * 将Markdown文本转换为HTML
     * @param markdown 原始Markdown文本
     * @return 转换后的HTML字符串
     */
    public String convert(String markdown) {
        // 1. 输入验证(修复空指针风险)
        if (markdown == null || markdown.trim().isEmpty()) {
            return "";
        }

        // 2. 缓存命中检查(性能优化)
        String cached = conversionCache.get(markdown);
        if (cached != null) {
            return cached;
        }

        // 3. 预处理:规范化换行符
        String normalized = markdown.replaceAll("\\r\\n|\\r", "\n");
        String[] lines = normalized.split("\n", -1);

        // 4. 解析块并转换
        List<Block> blocks = parseBlocks(lines);
        StringBuilder html = new StringBuilder();
        for (Block block : blocks) {
            html.append(convertBlock(block)).append("\n");
        }

        // 5. 清理多余换行并缓存结果
        String result = html.toString().replaceAll("\\n{3,}", "\n\n").trim();
        conversionCache.put(markdown, result);
        return result;
    }

    // ==================== 块解析模块(支持嵌套列表)====================
    private List<Block> parseBlocks(String[] lines) {
        List<Block> blocks = new ArrayList<>();
        List<String> currentBlockLines = new ArrayList<>();
        BlockType currentType = null;
        boolean inCodeBlock = false;
        String codeLanguage = "";

        for (String line : lines) {
            String trimmed = line.trim();

            // 处理代码块边界
            if (trimmed.startsWith("```")) {
                if (!inCodeBlock) {
                    flushCurrentBlock(blocks, currentBlockLines, currentType);
                    inCodeBlock = true;
                    codeLanguage = normalizeLanguage(trimmed.substring(3).trim());
                    currentType = BlockType.CODE_BLOCK;
                } else {
                    blocks.add(new CodeBlock(String.join("\n", currentBlockLines), codeLanguage));
                    currentBlockLines.clear();
                    inCodeBlock = false;
                    currentType = null;
                }
                continue;
            }

            if (inCodeBlock) {
                currentBlockLines.add(line);
                continue;
            }

            // 检测标题
            Matcher headingMatcher = HEADING_PATTERN.matcher(line);
            if (headingMatcher.find() && currentBlockLines.isEmpty()) {
                int level = headingMatcher.group(1).length();
                String content = headingMatcher.group(2).trim();
                blocks.add(new HeadingBlock(content, level));
                currentType = null;
                continue;
            }

            // 检测水平分割线
            if (HORIZONTAL_RULE_PATTERN.matcher(trimmed).matches() && currentBlockLines.isEmpty()) {
                flushCurrentBlock(blocks, currentBlockLines, currentType);
                blocks.add(new HorizontalRuleBlock());
                currentType = null;
                continue;
            }

            // 检测表格
            if (trimmed.contains("|") && currentType == null) {
                currentType = BlockType.TABLE;
                currentBlockLines.add(line);
                continue;
            }

            // 检测引用
            if (trimmed.startsWith(">") && currentType == null) {
                currentType = BlockType.QUOTE;
                currentBlockLines.add(trimmed.substring(1).trim());
                continue;
            }

            // 检测任务列表(必须在普通列表之前检测)
            Matcher taskListMatcher = TASK_LIST_PATTERN.matcher(line);
            if (taskListMatcher.find() && currentType == null) {
                currentType = BlockType.LIST;
                currentBlockLines.add(line);
                continue;
            }

            // 检测列表(有序和无序)
            Matcher orderedListMatcher = ORDERED_LIST_PATTERN.matcher(line);
            Matcher unorderedListMatcher = UNORDERED_LIST_PATTERN.matcher(line);
            if ((orderedListMatcher.find() || unorderedListMatcher.find()) && currentType == null) {
                currentType = BlockType.LIST;
                currentBlockLines.add(line);
                continue;
            }

            // 延续当前块(表格/引用/列表的后续行)
            if (currentType == BlockType.TABLE && trimmed.contains("|")) {
                currentBlockLines.add(line);
                continue;
            }
            if (currentType == BlockType.QUOTE && trimmed.startsWith(">")) {
                currentBlockLines.add(trimmed.substring(1).trim());
                continue;
            }
            if (currentType == BlockType.LIST && 
                (ORDERED_LIST_PATTERN.matcher(line).find() || 
                 UNORDERED_LIST_PATTERN.matcher(line).find() ||
                 TASK_LIST_PATTERN.matcher(line).find())) {
                currentBlockLines.add(line);
                continue;
            }

            // 空行结束当前块
            if (trimmed.isEmpty()) {
                flushCurrentBlock(blocks, currentBlockLines, currentType);
                currentType = null;
                continue;
            }

            // 普通段落
            currentBlockLines.add(line);
            if (currentType == null) {
                currentType = BlockType.PARAGRAPH;
            }
        }

        flushCurrentBlock(blocks, currentBlockLines, currentType);
        return blocks;
    }

    private void flushCurrentBlock(List<Block> blocks, List<String> lines, BlockType type) {
        if (lines.isEmpty()) return;
        String content = String.join("\n", lines);
        switch (type) {
            case TABLE: blocks.add(new TableBlock(content)); break;
            case LIST: blocks.add(new ListBlock(content)); break;
            case QUOTE: blocks.add(new QuoteBlock(content)); break;
            default: blocks.add(new ParagraphBlock(content));
        }
        lines.clear();
    }

    // ==================== 语言别名标准化 ====================
    private String normalizeLanguage(String lang) {
        if (lang == null || lang.isEmpty()) return "text";
        return languageAliases.getOrDefault(lang.toLowerCase(), lang.toLowerCase());
    }

    // ==================== 块转换模块 ====================
    private String convertBlock(Block block) {
        if (block instanceof HeadingBlock) {
            HeadingBlock hb = (HeadingBlock) block;
            String content = escapeHtmlIfNeeded(hb.getContent());
            return String.format("<h%d>%s</h%d>", hb.getLevel(), content, hb.getLevel());
        } else if (block instanceof CodeBlock) {
            CodeBlock cb = (CodeBlock) block;
            String content = escapeHtmlIfNeeded(cb.getContent());
            String langClass = cb.getLanguage().isEmpty() ? "" : 
                           String.format(" class=\"language-%s\"", cb.getLanguage());
            return String.format("<div class=\"code-block\">\n<pre><code%s>%s</code></pre>\n</div>", 
                              langClass, content);
        } else if (block instanceof TableBlock) {
            return convertTable((TableBlock) block);
        } else if (block instanceof ListBlock) {
            return convertList((ListBlock) block);
        } else if (block instanceof QuoteBlock) {
            QuoteBlock qb = (QuoteBlock) block;
            String content = escapeHtmlIfNeeded(qb.getContent());
            return String.format("<blockquote>\n%s\n</blockquote>", content);
        } else if (block instanceof HorizontalRuleBlock) {
            return "<hr>";
        } else {
            ParagraphBlock pb = (ParagraphBlock) block;
            String content = escapeHtmlIfNeeded(pb.getContent());
            content = processInlineFormatting(content);
            return String.format("<p>%s</p>", content);
        }
    }

    // ==================== 表格转换(支持行内格式)====================
    private String convertTable(TableBlock tb) {
        String[] lines = tb.getContent().split("\n", -1);
        if (lines.length < 2) return convertParagraph(new ParagraphBlock(tb.getContent()));

        // 解析表头
        String[] headers = parseTableRow(lines[0]);
        int columnCount = headers.length;

        // 解析对齐方式
        String[] aligns = parseAlignments(lines[1], columnCount);
        
        // 解析数据行
        List<String[]> rows = new ArrayList<>();
        for (int i = 2; i < lines.length; i++) {
            String line = lines[i].trim();
            if (!line.isEmpty() && line.contains("|")) {
                String[] cells = parseTableRow(line);
                if (cells.length > 0) {
                    rows.add(cells);
                }
            }
        }

        // 生成HTML
        StringBuilder html = new StringBuilder();
        html.append("<div class=\"table-container\">\n<table>\n<thead>\n<tr>");
        for (int i = 0; i < columnCount; i++) {
            String align = aligns[i];
            String header = escapeHtmlIfNeeded(headers[i]);
            header = processInlineFormatting(header); // 处理表头行内格式
            html.append(String.format("<th align=\"%s\">%s</th>", align, header));
        }
        html.append("</tr>\n</thead>\n<tbody>");
        for (String[] row : rows) {
            html.append("<tr>");
            for (int i = 0; i < Math.min(row.length, columnCount); i++) {
                String align = aligns[i];
                String cell = escapeHtmlIfNeeded(row[i]);
                cell = processInlineFormatting(cell); // 处理单元格行内格式
                html.append(String.format("<td align=\"%s\">%s</td>", align, cell));
            }
            html.append("</tr>");
        }
        html.append("</tbody>\n</table>\n</div>");
        return html.toString();
    }

    private String[] parseTableRow(String line) {
        String[] cells = line.split("\\|", -1);
        List<String> cleanedCells = new ArrayList<>();
        for (String cell : cells) {
            String trimmed = cell.trim();
            if (!trimmed.isEmpty()) {
                cleanedCells.add(trimmed);
            }
        }
        return cleanedCells.toArray(new String[0]);
    }

    private String[] parseAlignments(String separatorLine, int columnCount) {
        String[] separators = parseTableRow(separatorLine);
        String[] aligns = new String[columnCount];
        for (int i = 0; i < columnCount; i++) {
            String sep = i < separators.length ? separators[i] : "";
            if (sep.startsWith(":") && sep.endsWith(":")) {
                aligns[i] = "center";
            } else if (sep.startsWith(":")) {
                aligns[i] = "left";
            } else if (sep.endsWith(":")) {
                aligns[i] = "right";
            } else {
                aligns[i] = "left";
            }
        }
        return aligns;
    }

    // ==================== 列表转换(支持嵌套列表和任务列表)====================
    private String convertList(ListBlock lb) {
        String[] lines = lb.getContent().split("\n", -1);
        StringBuilder html = new StringBuilder();
        Deque<ListContext> stack = new ArrayDeque<>(); // 用于处理嵌套列表
        
        for (String line : lines) {
            String trimmed = line.trim();
            if (trimmed.isEmpty()) continue;

            // 检测任务列表
            Matcher taskMatcher = TASK_LIST_PATTERN.matcher(line);
            if (taskMatcher.find()) {
                handleListLine(html, stack, taskMatcher.group(1), taskMatcher.group(2), 
                             taskMatcher.group(3), taskMatcher.group(4), true);
                continue;
            }

            // 检测有序列表
            Matcher orderedMatcher = ORDERED_LIST_PATTERN.matcher(line);
            if (orderedMatcher.find()) {
                handleListLine(html, stack, orderedMatcher.group(1), "ol", 
                             orderedMatcher.group(2), orderedMatcher.group(3), false);
                continue;
            }

            // 检测无序列表
            Matcher unorderedMatcher = UNORDERED_LIST_PATTERN.matcher(line);
            if (unorderedMatcher.find()) {
                handleListLine(html, stack, unorderedMatcher.group(1), "ul", 
                             "", unorderedMatcher.group(2), false);
                continue;
            }
        }

        // 关闭所有未关闭的列表标签
        while (!stack.isEmpty()) {
            html.append(stack.pop().closeTag);
        }

        return html.toString();
    }

    /**
     * 处理列表行(支持嵌套)
     */
    private void handleListLine(StringBuilder html, Deque<ListContext> stack, 
                             String indent, String listType, String number, 
                             String content, boolean isTaskList) {
        int currentLevel = indent.length() / 2; // 每2个空格算一级缩进
        
        // 关闭比当前级别深的列表
        while (!stack.isEmpty() && stack.peek().level > currentLevel) {
            html.append(stack.pop().closeTag);
        }

        // 如果当前级别没有列表,则打开新列表
        if (stack.isEmpty() || stack.peek().level < currentLevel) {
            String openTag = listType.equals("ol") ? "<ol>" : "<ul>";
            stack.push(new ListContext(currentLevel, openTag, "</" + listType + ">"));
            html.append(openTag);
        }

        // 处理任务列表的特殊HTML
        String listItem;
        if (isTaskList) {
            String checked = number.equalsIgnoreCase("x") ? "checked" : "";
            listItem = String.format("<li><input type=\"checkbox\" %s disabled> %s</li>", 
                                   checked, escapeHtmlIfNeeded(content));
        } else {
            listItem = String.format("<li>%s</li>", escapeHtmlIfNeeded(content));
        }
        
        html.append(processInlineFormatting(listItem));
    }

    // ==================== 行内格式处理(支持图片)====================
    private String processInlineFormatting(String text) {
        // 处理图片(必须在链接之前处理,因为图片语法类似链接)
        text = IMAGE_PATTERN.matcher(text).replaceAll("<img src=\"$2\" alt=\"$1\" />");
        
        // 处理粗体
        text = INLINE_BOLD_PATTERN.matcher(text).replaceAll("<strong>$1$2</strong>");
        
        // 处理斜体
        text = INLINE_ITALIC_PATTERN.matcher(text).replaceAll("<em>$1$2</em>");
        
        // 处理行内代码
        text = INLINE_CODE_PATTERN.matcher(text).replaceAll("<code>$1</code>");
        
        // 处理链接
        text = LINK_PATTERN.matcher(text).replaceAll("<a href=\"$2\">$1</a>");
        
        return text;
    }

    // ==================== 辅助方法 ====================
    private String escapeHtmlIfNeeded(String text) {
        if (!enableEscapeHtml) return text;
        return text.replace("&", "&amp;")
                  .replace("<", "&lt;")
                  .replace(">", "&gt;")
                  .replace("\"", "&quot;")
                  .replace("'", "&#39;");
    }

    private String convertParagraph(ParagraphBlock pb) {
        String content = escapeHtmlIfNeeded(pb.getContent());
        content = processInlineFormatting(content);
        return String.format("<p>%s</p>", content);
    }

    // ==================== 嵌套列表上下文类 ====================
    private static class ListContext {
        final int level;
        final String openTag;
        final String closeTag;
        
        ListContext(int level, String openTag, String closeTag) {
            this.level = level;
            this.openTag = openTag;
            this.closeTag = closeTag;
        }
    }

    // ==================== 块类型定义 ====================
    private enum BlockType {
        PARAGRAPH, HEADING, CODE_BLOCK, TABLE, LIST, QUOTE, HORIZONTAL_RULE
    }

    private abstract static class Block {
        protected final String content;
        public Block(String content) { this.content = content; }
        public String getContent() { return content; }
    }

    private static class HeadingBlock extends Block {
        private final int level;
        public HeadingBlock(String content, int level) {
            super(content);
            this.level = level;
        }
        public int getLevel() { return level; }
    }

    private static class CodeBlock extends Block {
        private final String language;
        public CodeBlock(String content, String language) {
            super(content);
            this.language = language;
        }
        public String getLanguage() { return language; }
    }

    private static class TableBlock extends Block {
        public TableBlock(String content) { super(content); }
    }

    private static class ListBlock extends Block {
        public ListBlock(String content) { super(content); }
    }

    private static class QuoteBlock extends Block {
        public QuoteBlock(String content) { super(content); }
    }

    private static class ParagraphBlock extends Block {
        public ParagraphBlock(String content) { super(content); }
    }

    private static class HorizontalRuleBlock extends Block {
        public HorizontalRuleBlock() { super(""); }
    }
}

二、Spring Boot项目完整测试方案

1. 项目结构

复制代码
markdown-converter/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── markdown/
│   │   │               ├── MarkdownApplication.java
│   │   │               ├── controller/
│   │   │               │   └── MarkdownController.java
│   │   │               ├── service/
│   │   │               │   └── MarkdownToHtmlConverter.java
│   │   │               └── dto/
│   │   │                   ├── ConvertRequest.java
│   │   │                   └── ConvertResponse.java
│   │   └── resources/
│   │       └── application.yml
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── markdown/
│                       ├── MarkdownApplicationTests.java
│                       ├── service/
│                       │   └── MarkdownToHtmlConverterTest.java
│                       └── controller/
│                           └── MarkdownControllerTest.java

2. 启动类

java 复制代码
package com.example.markdown;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MarkdownApplication {
    public static void main(String[] args) {
        SpringApplication.run(MarkdownApplication.class, args);
    }
}

3. DTO类

java 复制代码
package com.example.markdown.dto;

public class ConvertRequest {
    private String markdown;
    
    // Getters and Setters
    public String getMarkdown() {
        return markdown;
    }
    
    public void setMarkdown(String markdown) {
        this.markdown = markdown;
    }
}

public class ConvertResponse {
    private String html;
    private boolean success;
    private String message;
    
    // Constructors
    public ConvertResponse() {}
    
    public ConvertResponse(String html, boolean success, String message) {
        this.html = html;
        this.success = success;
        this.message = message;
    }
    
    // Getters and Setters
    public String getHtml() {
        return html;
    }
    
    public void setHtml(String html) {
        this.html = html;
    }
    
    public boolean isSuccess() {
        return success;
    }
    
    public void setSuccess(boolean success) {
        this.success = success;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
}

4. 控制器

java 复制代码
package com.example.markdown.controller;

import com.example.markdown.dto.ConvertRequest;
import com.example.markdown.dto.ConvertResponse;
import com.example.markdown.service.MarkdownToHtmlConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/markdown")
@CrossOrigin(origins = "*")
public class MarkdownController {

    @Autowired
    private MarkdownToHtmlConverter converter;

    /**
     * 将Markdown转换为HTML
     */
    @PostMapping("/convert")
    public ResponseEntity<ConvertResponse> convert(@RequestBody ConvertRequest request) {
        try {
            String html = converter.convert(request.getMarkdown());
            return ResponseEntity.ok(new ConvertResponse(html, true, "转换成功"));
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(new ConvertResponse(null, false, "转换失败: " + e.getMessage()));
        }
    }

    /**
     * 健康检查端点
     */
    @GetMapping("/health")
    public ResponseEntity<String> health() {
        return ResponseEntity.ok("Markdown Converter Service is running");
    }
}

5. 配置文件

yaml 复制代码
# application.yml
server:
  port: 8080

spring:
  application:
    name: markdown-converter

# 日志配置
logging:
  level:
    com.example.markdown: DEBUG

6. 单元测试(服务层)

java 复制代码
package com.example.markdown.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class MarkdownToHtmlConverterTest {

    private MarkdownToHtmlConverter converter;

    @BeforeEach
    void setUp() {
        converter = new MarkdownToHtmlConverter(true);
    }

    @Test
    void testHeadingConversion() {
        String markdown = "# 标题1\n## 标题2";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("<h1>标题1</h1>"));
        assertTrue(html.contains("<h2>标题2</h2>"));
    }

    @Test
    void testNestedListConversion() {
        String markdown = "- 项目1\n  - 子项目1\n  - 子项目2\n- 项目2";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("<ul>"));
        assertTrue(html.contains("<li>项目1</li>"));
        assertTrue(html.contains("<ul>")); // 嵌套列表
        assertTrue(html.contains("<li>子项目1</li>"));
    }

    @Test
    void testTaskListConversion() {
        String markdown = "- [ ] 未完成任务\n- [x] 已完成任务";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("type=\"checkbox\""));
        assertTrue(html.contains("checked"));
    }

    @Test
    void testImageConversion() {
        String markdown = "!https://example.com/image.jpg";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("<img src=\"https://example.com/image.jpg\" alt=\"Alt Text\" />"));
    }

    @Test
    void testTableWithInlineFormatting() {
        String markdown = "| **粗体** | *斜体* |\n|---------|--------|\n| `代码` | 普通文本 |";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("<strong>粗体</strong>"));
        assertTrue(html.contains("<em>斜体</em>"));
        assertTrue(html.contains("<code>代码</code>"));
    }

    @Test
    void testCodeBlockWithLanguage() {
        String markdown = "```javascript\nconsole.log('Hello');\n```";
        String html = converter.convert(markdown);
        
        assertTrue(html.contains("language-javascript"));
    }

    @Test
    void testEmptyInput() {
        String html = converter.convert("");
        assertEquals("", html);
    }

    @Test
    void testNullInput() {
        String html = converter.convert(null);
        assertEquals("", html);
    }
}

7. 集成测试(控制器层)

java 复制代码
package com.example.markdown.controller;

import com.example.markdown.MarkdownApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(classes = MarkdownApplication.class, 
                 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MarkdownControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void testConvertEndpoint() {
        String url = "http://localhost:" + port + "/api/markdown/convert";
        
        String markdown = "# 测试标题\n\n这是一个**粗体**文本。";
        ResponseEntity<String> response = restTemplate.postForEntity(url, markdown, String.class);
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertTrue(response.getBody().contains("测试标题"));
        assertTrue(response.getBody().contains("<strong>粗体</strong>"));
    }

    @Test
    void testHealthEndpoint() {
        String url = "http://localhost:" + port + "/api/markdown/health";
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("Markdown Converter Service is running", response.getBody());
    }
}

8. 应用测试类

java 复制代码
package com.example.markdown;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MarkdownApplicationTests {

    @Test
    void contextLoads() {
        // 测试Spring上下文是否正确加载
    }
}

三、API测试方法

1. 使用curl测试

bash 复制代码
# 测试转换接口
curl -X POST http://localhost:8080/api/markdown/convert \
  -H "Content-Type: text/plain" \
  -d "# 测试标题\n\n这是**粗体**文本。"

# 测试健康检查
curl http://localhost:8080/api/markdown/health

2. 使用Postman测试

  • POST http://localhost:8080/api/markdown/convert
  • Headers: Content-Type: text/plain
  • Body: 原始Markdown文本

3. 使用Swagger UI(可选)

添加Springfox或SpringDoc OpenAPI依赖,自动生成API文档。

四、部署注意事项

  1. 字符编码:确保使用UTF-8编码处理中文
  2. 安全过滤 :生产环境建议启用enableEscapeHtml=true
  3. 性能监控:监控大文档转换的内存使用和耗时
  4. 缓存策略:对于频繁转换的内容使用缓存
  5. 错误处理:添加全局异常处理器

这套完整的Spring Boot项目提供了从工具类到API接口的完整实现,包含详细的注释和全面的测试用例,可以直接用于生产环境。

相关推荐
北暮城南1 小时前
使用 nvm 安装与管理多版本 Node.js(Windows)
windows·npm·node.js·nvm
阿维的博客日记1 小时前
传统 Spring XML 配置 vs Spring Boot Starter 对比文档
xml·spring boot·spring
xiaoshuaishuai82 小时前
C# 继承与虚方法
开发语言·windows·c#
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_31:(AbortSignal 深入解析与高级中止模式)
前端·ui·html·音视频·视频编解码
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_30:(AbortController 实现可取消的异步请求)
前端·ui·html·edge浏览器·媒体
代码漫谈2 小时前
JVM 参数调优:Spring Boot与JDK新特性的最佳结合
java·jvm·spring boot
北风朝向2 小时前
springboot使用@Validated校验List接口参数
spring boot·后端·list·校验·valid
阿赛工作室2 小时前
基于Vue3和TensorFlow.js的数字图像识别应用HTML单文件
javascript·html·tensorflow
сокол2 小时前
【网安-Web渗透测试-内网渗透】内网横向移动——Impacket套件
服务器·windows·网络安全·系统安全