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标签的资料请关注脚本之家其它相关文章!