java 使用 flexmark-java 进行 markdown 文件生成对应的 html 代码

flexmark-java 的官方文档地址: 点击跳转
Flexmark-java 一个通过 java 语言即可实现将 markdown 内容转换为对应的 html 代码的库,其对 markdown 拥有很好的一个支持。在进行博客构建之时,遇到的一个重要问题就是将 markdown 文件渲染为对应的 html 内容呈现出来。最初在前端实现是使用的 marked 库, 由于自己对于前端的使用并不够熟练,所以在进行很多定制化的渲染 markdown 的内容时,都需要去研究前端的一些逻辑和实现,变得十分麻烦,同时,对于后端的接口数据返回上,直接 返回 markdown 内容也不是很安全,所以便有了,在后端进行实现对应的转换逻辑比较好。
Flexmark-java 在网上详细的使用文档,感觉都比较少,对于官网给出的使用文档介绍的比较粗糙。所以,我将在这里记录一些自己对于 flexmark 的使用总结。
开胃菜
引入 flexmark-java 的 maven 依赖,在 pom.xml 文件中,添加如下内容:
<!-- markdown 文件转为 html 文件-->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.64.8</version>
</dependency>
测试用例:
package com.zj.zs.markdown;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.emoji.EmojiExtension;
import com.vladsch.flexmark.ext.emoji.EmojiImageType;
import com.vladsch.flexmark.ext.emoji.EmojiShortcutType;
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension;
import com.vladsch.flexmark.ext.toc.TocExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.data.MutableDataSet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName MarkdownTest
* @Author zj
* @Description markdown 文件测试
* @Date 2024/3/17 16:00
* @Version v1.0
**/
public class MarkdownTest {
final private static DataHolder OPTIONS = new MutableDataSet().set(Parser.EXTENSIONS, Arrays.asList(
TocExtension.create(),
// 自定义扩展,为<pre>标签添加line-numbers的class,用于prism库代码左侧行号展示
AutolinkExtension.create(),
EmojiExtension.create(),
StrikethroughExtension.create(),
TaskListExtension.create(),
TablesExtension.create()
))// set GitHub table parsing options
.set(TablesExtension.WITH_CAPTION, false)
.set(TablesExtension.COLUMN_SPANS, false)
.set(TablesExtension.MIN_HEADER_ROWS, 1)
.set(TablesExtension.MAX_HEADER_ROWS, 1)
.set(TablesExtension.APPEND_MISSING_COLUMNS, true)
.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)
.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
// setup emoji shortcut options
// uncomment and change to your image directory for emoji images if you have it setup
// .set(EmojiExtension.ROOT_IMAGE_PATH, emojiInstallDirectory())
.set(EmojiExtension.USE_SHORTCUT_TYPE, EmojiShortcutType.GITHUB)
.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.IMAGE_ONLY);
static final Parser PARSER = Parser.builder(OPTIONS).build();
static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS)
// 缩进 2 字符
.indentSize(2)
.build();
public static void main(String[] args) throws IOException {
List<String> lines = Files.readAllLines(Path.of("/Users/zj/IdeaProjects/work/zjBootBlog/README.md"));
String content = String.join("\n", lines);
Document document = PARSER.parse(content);
String html = RENDERER.render(document);
System.out.println("generate html : " + html);
}
}
上面的代码可以将 README.md 中的 markdown 文件内容生成对应的 html 代码、
例如我的 README.md 文件内容为:
# 个人使用的 spring boot 脚手架
## 项目描述
## 表
```sql
create table zs_article
(
title varchar(255) default '' null comment '标题',
id bigint auto_increment comment '主键' primary key,
content longtext null comment '内容',
create_time datetime default NOW() null comment '创建时间'
) comment '文章页面';
```

生成出来的 html 代码为:
<h1 id="个人使用的-spring-boot-脚手架">个人使用的 spring boot 脚手架</h1>
<h2 id="项目描述">项目描述</h2>
<h2 id="表">表</h2>
<pre><code class="language-sql">create table zs_article
(
title varchar(255) default '' null comment '标题',
id bigint auto_increment comment '主键' primary key,
content longtext null comment '内容',
create_time datetime default NOW() null comment '创建时间'
) comment '文章页面';
</code></pre>
<p><img src="https://zj134-file.cpolar.cn/file/zbus/blog202405102246409.jpg" alt="1121212" /></p>
自定义对于图片的渲染逻辑
比如上面我们生成的图片代码,需要在对应的 img 标签,放在一个容器 div 中,同时给它设置一些,定制化的 class 属性,比如,自定以的类,和图片放大缩小所用到的属性 data-zoomable (参考medium-zoom 的使用,或者访问我的介绍文章: vue3+ts 实现图片点击放大缩小的效果)。
那么对于 flexmark 如何实现呢,即自定义 图片的解析逻辑,这里我们就需要使用到 NodeRenderer 类进行实现。实现 NodeRenderingHandler.CustomNodeRenderer<T> 接口。其中的 render 方法就是我们进行自定义逻辑的实现,如下:
public class ImageCustomNodeRenderer implements NodeRenderingHandler.CustomNodeRenderer<Image> {
@Override
public void render(@NotNull Image node, @NotNull NodeRendererContext context, @NotNull HtmlWriter html) {
html.line();
// 渲染包裹图片的 div 标签
html.withAttr()
.attr("class", "zj-blog-content-img-container")
.tag("div");
html.line();
html.indent();
// 开始渲染图片标签
html.srcPosWithTrailingEOL(node.getChars()).withAttr()
.attr("class", "zj-blog-content-img")
.attr("data-zoomable", "")
.attr("src", node.getUrl())
.attr("alt", node.getText())
.tag("img");
html.line();
// 关闭图片 img
html.tag("/img");
// 新起一行
html.line();
// 关闭 div
html.tag("/div");
html.line();
}
}
实现了 NodeRenderingHandler.CustomNodeRenderer 接口之后,需要将这个自定义的节点解析类注入到我们的 html 解析器配置 (即: OPTIONS 配置),注入的方式是使用 HtmlRendererExtension 的方式,
比如我们这里自定义实现的图片扩展器。
package com.zj.zs.service.markdown.extension.img;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.util.data.MutableDataHolder;
import org.jetbrains.annotations.NotNull;
/**
* @ClassName CustomExtension
* @Author zj
* @Description
* @Date 2024/5/31 08:28
* @Version v1.0
**/
public class CustomImageExtension implements HtmlRenderer.HtmlRendererExtension {
@Override
public void rendererOptions(@NotNull MutableDataHolder options) {
}
@Override
public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, @NotNull String rendererType) {
htmlRendererBuilder.nodeRendererFactory(new CustomImageNodeRenderer.Factory());
}
public static CustomImageExtension create() {
return new CustomImageExtension();
}
}
上面的类中,我们使用了 CustomImageNodeRenderer 类来承载刚刚我们自定的解析逻辑,其具体的实现逻辑如下:
package com.zj.zs.service.markdown.extension.img;
import com.vladsch.flexmark.ast.Image;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.html.renderer.NodeRenderer;
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
import com.vladsch.flexmark.util.data.DataHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
/**
* @ClassName CustomImageNodeRenderer
* @Author zj
* @Description
* @Date 2024/6/1 10:35
* @Version v1.0
**/
public class CustomImageNodeRenderer implements NodeRenderer {
@Override
public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
HashSet<NodeRenderingHandler<?>> set = new HashSet<>();
set.add(imageNodeRenderingHandler());
return set;
}
private NodeRenderingHandler<Image> imageNodeRenderingHandler() {
return new NodeRenderingHandler<>(Image.class, new ImageCustomNodeRenderer());
}
public static class Factory implements NodeRendererFactory {
@NotNull
@Override
public NodeRenderer apply(@NotNull DataHolder options) {
return new CustomImageNodeRenderer();
}
}
}
再回到我们的解析代码,把 CustomImageExtension 注入即可实现对应的自定义图片解析逻辑。具体的代码是,如下:
package com.zj.zs.markdown;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.emoji.EmojiExtension;
import com.vladsch.flexmark.ext.emoji.EmojiImageType;
import com.vladsch.flexmark.ext.emoji.EmojiShortcutType;
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension;
import com.vladsch.flexmark.ext.toc.TocExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.zj.zs.service.markdown.extension.img.CustomImageExtension;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import static com.zj.zs.service.markdown.extension.toc.CustomTocNodeRenderer.TOC_HTML;
/**
* @ClassName MarkdownTest
* @Author zj
* @Description markdown 文件测试
* @Date 2024/3/17 16:00
* @Version v1.0
**/
public class MarkdownTest {
final private static DataHolder OPTIONS = new MutableDataSet().set(Parser.EXTENSIONS, Arrays.asList(
TocExtension.create(),
// 自定义的图片解析扩展器
CustomImageExtension.create(),
// 自定义扩展,为<pre>标签添加line-numbers的class,用于prism库代码左侧行号展示
AutolinkExtension.create(),
EmojiExtension.create(),
StrikethroughExtension.create(),
TaskListExtension.create(),
TablesExtension.create()
))// set GitHub table parsing options
.set(TablesExtension.WITH_CAPTION, false)
.set(TablesExtension.COLUMN_SPANS, false)
.set(TablesExtension.MIN_HEADER_ROWS, 1)
.set(TablesExtension.MAX_HEADER_ROWS, 1)
.set(TablesExtension.APPEND_MISSING_COLUMNS, true)
.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)
.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
// setup emoji shortcut options
// uncomment and change to your image directory for emoji images if you have it setup
// .set(EmojiExtension.ROOT_IMAGE_PATH, emojiInstallDirectory())
.set(EmojiExtension.USE_SHORTCUT_TYPE, EmojiShortcutType.GITHUB)
.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.IMAGE_ONLY);
static final Parser PARSER = Parser.builder(OPTIONS).build();
static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS)
// 缩进 2 字符
.indentSize(2)
.build();
public static void main(String[] args) throws IOException {
List<String> lines = Files.readAllLines(Path.of("/Users/zj/IdeaProjects/work/zjBootBlog/README.md"));
String content = String.join("\n", lines);
Document document = PARSER.parse(content);
String html = RENDERER.render(document);
System.out.println("test html : " + html);
}
}
最终我们生成的代码如下:
<h1 id="个人使用的-spring-boot-脚手架">个人使用的 spring boot 脚手架</h1>
<h2 id="项目描述">项目描述</h2>
<h2 id="表">表</h2>
<pre><code class="language-sql">create table zs_article
(
title varchar(255) default '' null comment '标题',
id bigint auto_increment comment '主键' primary key,
content longtext null comment '内容',
create_time datetime default NOW() null comment '创建时间'
) comment '文章页面';
</code></pre>
<p>
<div class="zj-blog-content-img-container">
<img class="zj-blog-content-img" data-zoomable="" src="https://zj134-file.cpolar.cn/file/zbus/blog202405102246409.jpg"
alt="1121212">
</img>
</div>
</p>
可以看到上面,我们的图片渲染成功的添加了一个 div 包裹,并且包含了自定义的 class 属性和 data-zoomable 属性。
自定义代码渲染逻辑
其实现过程和图片的实现逻辑类似。这里直接上代码,CustomPreCodeExtension 类主要的扩展类。