SpringBoot实现Markdown语法转HTML标签的详细步骤
作者:无缘之缘
这篇文章主要介绍了SpringBoot实现Markdown转HTML的步骤,涵盖项目配置、测试语法扩展(自动链接、表情、表格等)、解决GitLab和脚注问题,并添加自定义解析器以确保正确解析,需要的朋友可以参考下
1、项目启动
pom.xml文件
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.1</version>
</parent>
<dependencies>
<!-- 将 Markdown 转换为 HTML -->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.62.2</version>
</dependency>
<!--以下为基础依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
核心文件
public class MarkdownConverter {
// 定义一个静态方法,将 Markdown 文本转换为 HTML
public static String markdownToHtml(String markdown) {
// 创建一个 MutableDataSet 对象来配置 Markdown 解析器的选项
MutableDataSet options = new MutableDataSet();
// 添加各种 Markdown 解析器的扩展
options.set(Parser.EXTENSIONS, Arrays.asList(
AutolinkExtension.create(), // 自动链接扩展,将URL文本转换为链接(如 http://example.com)自动变为 <a href="..." rel="external nofollow" >
EmojiExtension.create(), // 表情符号扩展,用于解析表情符号(如 :smile: 转换为 😄)
GitLabExtension.create(), // GitLab特有的Markdown扩展(如支持 ~~删除线~~)
FootnoteExtension.create(), // 脚注扩展,用于添加和解析脚注(如 [^1])
TaskListExtension.create(), // 任务列表扩展,用于创建任务列表(如 - [x] 已完成)
// CustomAdmonitionExtension.create(), // 提示框扩展,用于创建提示框(如 !!! note 创建提示框)
TablesExtension.create())); // 表格扩展,用于解析和渲染表格(如 | 表头1 | 表头2 |)
// 使用配置的选项构建一个 Markdown 解析器
Parser parser = Parser.builder(options).build();//Markdown-->抽象语法树
// 使用相同的选项构建一个 HTML 渲染器
HtmlRenderer renderer = HtmlRenderer.builder(options).build();//抽象语法树-->HTML
// 解析传入的 Markdown 文本并将其渲染为 HTML
return renderer.render(parser.parse(markdown));
}
}
controller代码
@RestController
public class MarkdownController {
@PostMapping("/test")
public String markdownToHtml(String content) {
return MarkdownConverter.markdownToHtml(content);
}
}
启动入口
@SpringBootApplication
public class RunApplication {
public static void main(String[] args) {
SpringApplication.run(RunApplication.class, args);
}
}
2、运行测试
2.1 基础markdown语法测试
输入内容
# 标题1 ## 标题2 **粗体文本** *斜体文本* `行内代码` > 引用文本
结果如下

2.2 自动链接扩展测试文本
输入内容
访问我们的网站 http://example.com 获取更多信息
结果如下

2.3 表情符号扩展测试文本
输入内容
这是一个微笑表情 :smile: 和大笑表情 :laughing:
结果如下

2.4 GitLab扩展测试文本
输入内容:
这是删除线文本 ~~删除线~~
结果如下:
这里看到删除线并不能正确解析,该问题待解决

2.5 脚注扩展测试文本
输入内容:
这是一个带有脚注的文本[^1]. [^1]: 这是脚注的内容。
结果如下:

2.6 任务列表扩展测试文本
输入内容:
- [x] 已完成的任务 - [ ] 未完成的任务
结果如下:

2.7 表格扩展测试文本
输入内容:
| 表头1 | 表头2 | |-------|-------| | 内容1 | 内容2 |
结果如下:

3、自定义解析器
我们想解析
!!! note
这是一个提示框的内容
3.1 自定义解析器
增加以下两个类
public class CustomAdmonitionBlockParser extends AbstractBlockParser {
final private static String ADMONITION_START_FORMAT = "^(\\?{3}\\+|\\?{3}|!{3}|:{3})\\s*(%s)(?:\\s+(%s))?\\s*$";
final AdmonitionBlock block;
//private BlockContent content = new BlockContent();
final private AdmonitionOptions options;
final private int contentIndent;
private boolean hadBlankLine;
private boolean isOver;
CustomAdmonitionBlockParser(AdmonitionOptions options, int contentIndent) {
this.options = options;
this.contentIndent = contentIndent;
this.block = new AdmonitionBlock();
}
private int getContentIndent() {
return contentIndent;
}
@Override
public Block getBlock() {
return block;
}
@Override
public boolean isContainer() {
return true;
}
@Override
public boolean canContain(ParserState state, BlockParser blockParser, final Block block) {
return true;
}
@Override
public BlockContinue tryContinue(ParserState state) {
// 获取当前行内容
BasedSequence line = state.getLine();
final int nonSpaceIndex = state.getNextNonSpaceIndex();
// 判断是否是终止符 "!!!"
if (isOver) {
return BlockContinue.none();
}
if (line.startsWith("!!!") || line.startsWith("???") || line.startsWith(":::")) {
isOver = true;// 停止解析
}
// 如果当前行是空行,则继续解析,同时标记块中出现过空行
if (state.isBlank()) {
hadBlankLine = true;
return BlockContinue.atIndex(nonSpaceIndex);
}
// 如果允许懒惰继续(lazy continuation),且未遇到空行
if (!hadBlankLine && options.allowLazyContinuation) {
return BlockContinue.atIndex(nonSpaceIndex);
}
// 如果缩进足够,则继续解析当前行
if (state.getIndent() >= options.contentIndent) {
int contentIndent = state.getColumn() + options.contentIndent;
return BlockContinue.atColumn(contentIndent);
}
// 默认情况,继续解析当前行
return BlockContinue.atIndex(nonSpaceIndex);
}
@Override
public void closeBlock(ParserState state) {
block.setCharsFromContent();
}
public static class Factory implements CustomBlockParserFactory {
@Nullable
@Override
public Set<Class<?>> getAfterDependents() {
return null;
}
@Nullable
@Override
public Set<Class<?>> getBeforeDependents() {
return null;
}
@Override
public @Nullable SpecialLeadInHandler getLeadInHandler(@NotNull DataHolder options) {
return AdmonitionLeadInHandler.HANDLER;
}
@Override
public boolean affectsGlobalScope() {
return false;
}
@NotNull
@Override
public BlockParserFactory apply(@NotNull DataHolder options) {
return new BlockFactory(options);
}
}
static class AdmonitionLeadInHandler implements SpecialLeadInHandler {
final static SpecialLeadInHandler HANDLER = new AdmonitionLeadInHandler();
@Override
public boolean escape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer<CharSequence> consumer) {
if ((sequence.length() == 3 || sequence.length() == 4 && sequence.charAt(3) == '+') && (sequence.startsWith("???") || sequence.startsWith("!!!") || sequence.startsWith(":::"))) {
consumer.accept("\\");
consumer.accept(sequence);
return true;
}
return false;
}
@Override
public boolean unEscape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer<CharSequence> consumer) {
if ((sequence.length() == 4 || sequence.length() == 5 && sequence.charAt(4) == '+') && (sequence.startsWith("\\???") || sequence.startsWith("\\!!!") || sequence.startsWith("\\:::"))) {
consumer.accept(sequence.subSequence(1));
return true;
}
return false;
}
}
static boolean isMarker(
final ParserState state,
final int index,
final boolean inParagraph,
final boolean inParagraphListItem,
final AdmonitionOptions options
) {
final boolean allowLeadingSpace = options.allowLeadingSpace;
final boolean interruptsParagraph = options.interruptsParagraph;
final boolean interruptsItemParagraph = options.interruptsItemParagraph;
final boolean withLeadSpacesInterruptsItemParagraph = options.withSpacesInterruptsItemParagraph;
CharSequence line = state.getLine();
if (!inParagraph || interruptsParagraph) {
if ((allowLeadingSpace || state.getIndent() == 0) && (!inParagraphListItem || interruptsItemParagraph)) {
if (inParagraphListItem && !withLeadSpacesInterruptsItemParagraph) {
return state.getIndent() == 0;
} else {
return state.getIndent() < state.getParsing().CODE_BLOCK_INDENT;
}
}
}
return false;
}
private static class BlockFactory extends AbstractBlockParserFactory {
final private AdmonitionOptions options;
BlockFactory(DataHolder options) {
super(options);
this.options = new AdmonitionOptions(options);
}
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
if (state.getIndent() >= 4) {
return BlockStart.none();
}
int nextNonSpace = state.getNextNonSpaceIndex();
BlockParser matched = matchedBlockParser.getBlockParser();
boolean inParagraph = matched.isParagraphParser();
boolean inParagraphListItem = inParagraph && matched.getBlock().getParent() instanceof ListItem && matched.getBlock() == matched.getBlock().getParent().getFirstChild();
if (isMarker(state, nextNonSpace, inParagraph, inParagraphListItem, options)) {
BasedSequence line = state.getLine();
BasedSequence trySequence = line.subSequence(nextNonSpace, line.length());
Parsing parsing = state.getParsing();
Pattern startPattern = Pattern.compile(String.format(ADMONITION_START_FORMAT, parsing.ATTRIBUTENAME, parsing.LINK_TITLE_STRING));
Matcher matcher = startPattern.matcher(trySequence);
if (matcher.find()) {
// admonition block
BasedSequence openingMarker = line.subSequence(nextNonSpace + matcher.start(1), nextNonSpace + matcher.end(1));
BasedSequence info = line.subSequence(nextNonSpace + matcher.start(2), nextNonSpace + matcher.end(2));
BasedSequence titleChars = matcher.group(3) == null ? BasedSequence.NULL : line.subSequence(nextNonSpace + matcher.start(3), nextNonSpace + matcher.end(3));
int contentOffset = options.contentIndent;
CustomAdmonitionBlockParser admonitionBlockParser = new CustomAdmonitionBlockParser (options, contentOffset);
admonitionBlockParser.block.setOpeningMarker(openingMarker);
admonitionBlockParser.block.setInfo(info);
admonitionBlockParser.block.setTitleChars(titleChars);
return BlockStart.of(admonitionBlockParser)
.atIndex(line.length());
} else {
return BlockStart.none();
}
} else {
return BlockStart.none();
}
}
}
}
public class CustomAdmonitionExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, Formatter.FormatterExtension
// , Parser.ReferenceHoldingExtension
{
final public static DataKey<Integer> CONTENT_INDENT = new DataKey<>("ADMONITION.CONTENT_INDENT", 4);
final public static DataKey<Boolean> ALLOW_LEADING_SPACE = new DataKey<>("ADMONITION.ALLOW_LEADING_SPACE", true);
final public static DataKey<Boolean> INTERRUPTS_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_PARAGRAPH", true);
final public static DataKey<Boolean> INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_ITEM_PARAGRAPH", true);
final public static DataKey<Boolean> WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH", true);
final public static DataKey<Boolean> ALLOW_LAZY_CONTINUATION = new DataKey<>("ADMONITION.ALLOW_LAZY_CONTINUATION", true);
final public static DataKey<String> UNRESOLVED_QUALIFIER = new DataKey<>("ADMONITION.UNRESOLVED_QUALIFIER", "note");
final public static DataKey<Map<String, String>> QUALIFIER_TYPE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TYPE_MAP", CustomAdmonitionExtension::getQualifierTypeMap);
final public static DataKey<Map<String, String>> QUALIFIER_TITLE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TITLE_MAP", CustomAdmonitionExtension::getQualifierTitleMap);
final public static DataKey<Map<String, String>> TYPE_SVG_MAP = new DataKey<>("ADMONITION.TYPE_SVG_MAP", CustomAdmonitionExtension::getQualifierSvgValueMap);
public static Map<String, String> getQualifierTypeMap() {
HashMap<String, String> infoSvgMap = new HashMap<>();
// qualifier type map
infoSvgMap.put("abstract", "abstract");
infoSvgMap.put("summary", "abstract");
infoSvgMap.put("tldr", "abstract");
infoSvgMap.put("bug", "bug");
infoSvgMap.put("danger", "danger");
infoSvgMap.put("error", "danger");
infoSvgMap.put("example", "example");
infoSvgMap.put("snippet", "example");
infoSvgMap.put("fail", "fail");
infoSvgMap.put("failure", "fail");
infoSvgMap.put("missing", "fail");
infoSvgMap.put("faq", "faq");
infoSvgMap.put("question", "faq");
infoSvgMap.put("help", "faq");
infoSvgMap.put("info", "info");
infoSvgMap.put("todo", "info");
infoSvgMap.put("note", "note");
infoSvgMap.put("seealso", "note");
infoSvgMap.put("quote", "quote");
infoSvgMap.put("cite", "quote");
infoSvgMap.put("success", "success");
infoSvgMap.put("check", "success");
infoSvgMap.put("done", "success");
infoSvgMap.put("tip", "tip");
infoSvgMap.put("hint", "tip");
infoSvgMap.put("important", "tip");
infoSvgMap.put("warning", "warning");
infoSvgMap.put("caution", "warning");
infoSvgMap.put("attention", "warning");
return infoSvgMap;
}
public static Map<String, String> getQualifierTitleMap() {
HashMap<String, String> infoTitleMap = new HashMap<>();
infoTitleMap.put("abstract", "Abstract");
infoTitleMap.put("summary", "Summary");
infoTitleMap.put("tldr", "TLDR");
infoTitleMap.put("bug", "Bug");
infoTitleMap.put("danger", "Danger");
infoTitleMap.put("error", "Error");
infoTitleMap.put("example", "Example");
infoTitleMap.put("snippet", "Snippet");
infoTitleMap.put("fail", "Fail");
infoTitleMap.put("failure", "Failure");
infoTitleMap.put("missing", "Missing");
infoTitleMap.put("faq", "Faq");
infoTitleMap.put("question", "Question");
infoTitleMap.put("help", "Help");
infoTitleMap.put("info", "Info");
infoTitleMap.put("todo", "To Do");
infoTitleMap.put("note", "Note");
infoTitleMap.put("seealso", "See Also");
infoTitleMap.put("quote", "Quote");
infoTitleMap.put("cite", "Cite");
infoTitleMap.put("success", "Success");
infoTitleMap.put("check", "Check");
infoTitleMap.put("done", "Done");
infoTitleMap.put("tip", "Tip");
infoTitleMap.put("hint", "Hint");
infoTitleMap.put("important", "Important");
infoTitleMap.put("warning", "Warning");
infoTitleMap.put("caution", "Caution");
infoTitleMap.put("attention", "Attention");
return infoTitleMap;
}
public static Map<String, String> getQualifierSvgValueMap() {
HashMap<String, String> typeSvgMap = new HashMap<>();
typeSvgMap.put("abstract", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-abstract.svg")));
typeSvgMap.put("bug", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-bug.svg")));
typeSvgMap.put("danger", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-danger.svg")));
typeSvgMap.put("example", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-example.svg")));
typeSvgMap.put("fail", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-fail.svg")));
typeSvgMap.put("faq", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-faq.svg")));
typeSvgMap.put("info", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-info.svg")));
typeSvgMap.put("note", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-note.svg")));
typeSvgMap.put("quote", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-quote.svg")));
typeSvgMap.put("success", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-success.svg")));
typeSvgMap.put("tip", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-tip.svg")));
typeSvgMap.put("warning", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-warning.svg")));
return typeSvgMap;
}
public static String getInputStreamContent(InputStream inputStream) {
try {
InputStreamReader streamReader = new InputStreamReader(inputStream);
StringWriter stringWriter = new StringWriter();
copy(streamReader, stringWriter);
stringWriter.close();
return stringWriter.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
public static String getDefaultCSS() {
return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.css"));
}
public static String getDefaultScript() {
return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.js"));
}
public static void copy(Reader reader, Writer writer) throws IOException {
char[] buffer = new char[4096];
int n;
while (-1 != (n = reader.read(buffer))) {
writer.write(buffer, 0, n);
}
writer.flush();
reader.close();
}
private CustomAdmonitionExtension() {
}
public static CustomAdmonitionExtension create() {
return new CustomAdmonitionExtension();
}
@Override
public void extend(Formatter.Builder formatterBuilder) {
formatterBuilder.nodeFormatterFactory(new AdmonitionNodeFormatter.Factory());
}
@Override
public void rendererOptions(@NotNull MutableDataHolder options) {
}
@Override
public void parserOptions(MutableDataHolder options) {
}
@Override
public void extend(Parser.Builder parserBuilder) {
parserBuilder.customBlockParserFactory(new CustomAdmonitionBlockParser.Factory());
}
@Override
public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, @NotNull String rendererType) {
if (htmlRendererBuilder.isRendererType("HTML")) {
htmlRendererBuilder.nodeRendererFactory(new AdmonitionNodeRenderer.Factory());
} else if (htmlRendererBuilder.isRendererType("JIRA")) {
}
}
}
MarkdownConverter类中增加以下解析器
options.set(Parser.EXTENSIONS, Arrays.asList(
// ... 其他扩展
CustomAdmonitionExtension.create(), // 提示框扩展,用于创建提示框(如 !!! note 创建提示框)
));
3.2 测试自定义解析器
输入内容:
!!! note
这是一个提示框的内容
结果如下:

以上就是SpringBoot实现Markdown语法转HTML标签的详细步骤的详细内容,更多关于SpringBoot Markdown语法转HTML标签的资料请关注脚本之家其它相关文章!
