SpringBoot實現(xiàn)Markdown語法轉(zhuǎn)HTML標簽的詳細步驟
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 轉(zhuǎn)換為 HTML -->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.62.2</version>
</dependency>
<!--以下為基礎(chǔ)依賴-->
<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 {
// 定義一個靜態(tài)方法,將 Markdown 文本轉(zhuǎn)換為 HTML
public static String markdownToHtml(String markdown) {
// 創(chuàng)建一個 MutableDataSet 對象來配置 Markdown 解析器的選項
MutableDataSet options = new MutableDataSet();
// 添加各種 Markdown 解析器的擴展
options.set(Parser.EXTENSIONS, Arrays.asList(
AutolinkExtension.create(), // 自動鏈接擴展,將URL文本轉(zhuǎn)換為鏈接(如 http://example.com)自動變?yōu)?<a href="..." rel="external nofollow" >
EmojiExtension.create(), // 表情符號擴展,用于解析表情符號(如 :smile: 轉(zhuǎn)換為 ??)
GitLabExtension.create(), // GitLab特有的Markdown擴展(如支持 ~~刪除線~~)
FootnoteExtension.create(), // 腳注擴展,用于添加和解析腳注(如 [^1])
TaskListExtension.create(), // 任務(wù)列表擴展,用于創(chuàng)建任務(wù)列表(如 - [x] 已完成)
// CustomAdmonitionExtension.create(), // 提示框擴展,用于創(chuàng)建提示框(如 !!! note 創(chuàng)建提示框)
TablesExtension.create())); // 表格擴展,用于解析和渲染表格(如 | 表頭1 | 表頭2 |)
// 使用配置的選項構(gòu)建一個 Markdown 解析器
Parser parser = Parser.builder(options).build();//Markdown-->抽象語法樹
// 使用相同的選項構(gòu)建一個 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 基礎(chǔ)markdown語法測試
輸入內(nèi)容
# 標題1 ## 標題2 **粗體文本** *斜體文本* `行內(nèi)代碼` > 引用文本
結(jié)果如下

2.2 自動鏈接擴展測試文本
輸入內(nèi)容
訪問我們的網(wǎng)站 http://example.com 獲取更多信息
結(jié)果如下

2.3 表情符號擴展測試文本
輸入內(nèi)容
這是一個微笑表情 :smile: 和大笑表情 :laughing:
結(jié)果如下

2.4 GitLab擴展測試文本
輸入內(nèi)容:
這是刪除線文本 ~~刪除線~~
結(jié)果如下:
這里看到刪除線并不能正確解析,該問題待解決

2.5 腳注擴展測試文本
輸入內(nèi)容:
這是一個帶有腳注的文本[^1]. [^1]: 這是腳注的內(nèi)容。
結(jié)果如下:

2.6 任務(wù)列表擴展測試文本
輸入內(nèi)容:
- [x] 已完成的任務(wù) - [ ] 未完成的任務(wù)
結(jié)果如下:

2.7 表格擴展測試文本
輸入內(nèi)容:
| 表頭1 | 表頭2 | |-------|-------| | 內(nèi)容1 | 內(nèi)容2 |
結(jié)果如下:

3、自定義解析器
我們想解析
!!! note
這是一個提示框的內(nèi)容
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) {
// 獲取當前行內(nèi)容
BasedSequence line = state.getLine();
final int nonSpaceIndex = state.getNextNonSpaceIndex();
// 判斷是否是終止符 "!!!"
if (isOver) {
return BlockContinue.none();
}
if (line.startsWith("!!!") || line.startsWith("???") || line.startsWith(":::")) {
isOver = true;// 停止解析
}
// 如果當前行是空行,則繼續(xù)解析,同時標記塊中出現(xiàn)過空行
if (state.isBlank()) {
hadBlankLine = true;
return BlockContinue.atIndex(nonSpaceIndex);
}
// 如果允許懶惰繼續(xù)(lazy continuation),且未遇到空行
if (!hadBlankLine && options.allowLazyContinuation) {
return BlockContinue.atIndex(nonSpaceIndex);
}
// 如果縮進足夠,則繼續(xù)解析當前行
if (state.getIndent() >= options.contentIndent) {
int contentIndent = state.getColumn() + options.contentIndent;
return BlockContinue.atColumn(contentIndent);
}
// 默認情況,繼續(xù)解析當前行
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(), // 提示框擴展,用于創(chuàng)建提示框(如 !!! note 創(chuàng)建提示框)
));
3.2 測試自定義解析器
輸入內(nèi)容:
!!! note
這是一個提示框的內(nèi)容
結(jié)果如下:

以上就是SpringBoot實現(xiàn)Markdown語法轉(zhuǎn)HTML標簽的詳細步驟的詳細內(nèi)容,更多關(guān)于SpringBoot Markdown語法轉(zhuǎn)HTML標簽的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis-Plus非表字段的三種處理方法小結(jié)
這篇文章主要介紹了MyBatis-Plus非表字段的三種處理方法小結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
shiro并發(fā)人數(shù)登錄控制的實現(xiàn)代碼
在做項目中遇到這樣的需求要求每個賬戶同時只能有一個人登錄或幾個人同時登錄,如果是同時登錄的多人,要么不讓后者登錄,要么踢出前者登錄,怎么實現(xiàn)這樣的功能呢?下面小編給大家?guī)砹藄hiro并發(fā)人數(shù)登錄控制的實現(xiàn)代碼,一起看看吧2017-09-09
java智能問答圖靈機器人AI接口(聚合數(shù)據(jù))
這篇文章主要介紹了java智能問答圖靈機器人AI接口(聚合數(shù)據(jù)),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
mybatis配置文件簡介_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細介紹了mybatis配置文件簡介的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
Quartz+Spring Boot實現(xiàn)動態(tài)管理定時任務(wù)
最近做項目遇到動態(tài)管理定時任務(wù)的需求,剛拿到這個需求還真不知道從哪下手,經(jīng)過一番思考,終于找出實現(xiàn)思路,接下來通過本文給大家介紹了Quartz+Spring Boot實現(xiàn)動態(tài)管理定時任務(wù)的相關(guān)知識,需要的朋友可以參考下2018-09-09
interrupt()和線程終止方式_動力節(jié)點Java學(xué)院整理
線程的thread.interrupt()方法是中斷線程,將會設(shè)置該線程的中斷狀態(tài)位,即設(shè)置為true,中斷的結(jié)果線程是死亡、還是等待新的任務(wù)或是繼續(xù)運行至下一步,就取決于這個程序本身2017-05-05
java集合collection接口與子接口及實現(xiàn)類
這篇文章主要介紹了java集合collection接口與子接口及實現(xiàn)類,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-07-07

