当前位置: 首页 > news >正文

SpringBoot实现Markdown语法转HTML标签

大家也可以自己拷贝源码地址
GitHub源码仓库地址

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 文本转换为 HTMLpublic 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="...">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 文本并将其渲染为 HTMLreturn 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;}@Overridepublic Block getBlock() {return block;}@Overridepublic boolean isContainer() {return true;}@Overridepublic boolean canContain(ParserState state, BlockParser blockParser, final Block block) {return true;}@Overridepublic 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);}@Overridepublic void closeBlock(ParserState state) {block.setCharsFromContent();}public static class Factory implements CustomBlockParserFactory {@Nullable@Overridepublic Set<Class<?>> getAfterDependents() {return null;}@Nullable@Overridepublic Set<Class<?>> getBeforeDependents() {return null;}@Overridepublic @Nullable SpecialLeadInHandler getLeadInHandler(@NotNull DataHolder options) {return AdmonitionLeadInHandler.HANDLER;}@Overridepublic boolean affectsGlobalScope() {return false;}@NotNull@Overridepublic BlockParserFactory apply(@NotNull DataHolder options) {return new BlockFactory(options);}}static class AdmonitionLeadInHandler implements SpecialLeadInHandler {final static SpecialLeadInHandler HANDLER = new AdmonitionLeadInHandler();@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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 blockBasedSequence 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 mapinfoSvgMap.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();}@Overridepublic void extend(Formatter.Builder formatterBuilder) {formatterBuilder.nodeFormatterFactory(new AdmonitionNodeFormatter.Factory());}@Overridepublic void rendererOptions(@NotNull MutableDataHolder options) {}@Overridepublic void parserOptions(MutableDataHolder options) {}@Overridepublic void extend(Parser.Builder parserBuilder) {parserBuilder.customBlockParserFactory(new CustomAdmonitionBlockParser.Factory());}@Overridepublic 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这是一个提示框的内容

结果如下:
在这里插入图片描述

http://www.dtcms.com/a/389808.html

相关文章:

  • DeepSeek:大语言模型在中文生态中的技术突破与应用探索
  • 【Agent博客分享】从多Agent问题到新的上下文工程方法
  • 点云分割中 offset 与 batch 表示的转换详解
  • C++23 堆栈跟踪功能实战:从内存泄漏梦魇到一键定位的调试革命
  • jvm参数调优(持续更新)
  • 容器查看日志工具-stern
  • 衍射光学元件DOE:台阶高度与位置误差的测量
  • Java中对象/嵌套对象属性复制工具类使用示例:Hutools工具类BeanUtils使用示例
  • rust编写web服务02-路由与请求处理
  • Spring Cloud - 微服务限流的方式
  • 【智能系统项目开发与学习记录】ROS2基础(1)
  • 人工智能面试题:什么是CRF条件随机场
  • [x-cmd] 命令式交互、CLI/TUI 设计与 LLM
  • 基于AMBA总线协议的Verilog语言模型实现
  • 【Agent项目复现】OpenManus复现
  • 高校AI虚拟仿真实训平台软件解决方案
  • Vue3 + Ant Design Vue 实现统一禁用样式管理方案,禁用状态下已有值颜色区分(CSS 变量方案)
  • Ubuntu 24.04部署MongoDB
  • 8.1-spring 事务-声明式事务(使用)
  • Vue3》》组件继承 extends
  • 无人系统在边境管控的应用探讨
  • 一个典型的mysql数据库连接池初始化函数
  • novel英文单词学习
  • 数据结构:树及二叉树--堆(下)
  • TDengine 聚合函数 STDDEV 用户手册
  • ARM--启动代码
  • openharmony1.1.3 通过i2c进行温湿度采集
  • 虚拟仿真技术赋能国土资源监测教育,破解生态与安全人才培养困局
  • Vim 详细使用方法与运维工作常用操作
  • python基础数据分析与可视化