1. 简介
最近因为项目需求需要将AI输出的结果导出到word中, 但AI输出的格式为markdown格式,因为word展示内容的时候需要有相应的格式(标题, 段落, 列表, 表格等), 所以不能直接将markdown输出到word中, 否则word中展示的就是markdown纯文本了, 调研一番后发现如果想要word展示效果好一点的话需要分成两步
- 将
markdown→html - 将
html→ooxml(Office Open XML) word内容,word元信息本身就是个xml)
所以本章先实现第一步 markdown → html, 使用的组件为flexmark
2. 环境信息
为了兼容更多的场景, 所以并没有用一些高版本的SDK, 信息如下
Java: 8 Flexmark: 0.60.2
3. Maven
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ldx</groupId> <artifactId>md2html</artifactId> <version>1.0-SNAPSHOT</version> <properties> <flexmark.version>0.60.2</flexmark.version> </properties> <dependencies> <dependency> <groupId>com.vladsch.flexmark</groupId> <artifactId>flexmark</artifactId> <version>${flexmark.version}</version> </dependency> <dependency> <groupId>com.vladsch.flexmark</groupId> <artifactId>flexmark-ext-tables</artifactId> <version>${flexmark.version}</version> </dependency> </dependencies> </project>
4. Markdown转Html
import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.data.MutableDataSet; public class MarkdownToHtml { public static String convertMarkdownToHtml(String markdown) { // 创建配置集 MutableDataSet options = new MutableDataSet(); // 创建解析器和渲染器 Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); // 解析 Markdown 文本 Node document = parser.parse(markdown); // 渲染为 HTML return renderer.render(document); } public static void main(String[] args) { String markdown = "## 嘉文四世n" + "n" + "> 德玛西亚n" + "n" + "**给我找些更强的敌人!**"; final String html = convertMarkdownToHtml(markdown); System.out.println(html); } }
测试结果如下:
<h2>嘉文四世</h2> <blockquote> <p>德玛西亚</p> </blockquote> <p><strong>给我找些更强的敌人!</strong></p>
5. 高级用法
5.1 启用Table扩展
flexmark 支持多种扩展,需要通过 Extension 注册, 比如启用表格语法, flexmark默认没有启用表格语法比如测试
public static void main(String[] args) { String markdown = "| 列1 | 列2 |n" + "| ----- | ----- |n" + "| 数据1 | 数据2 |"; final String html = convertMarkdownToHtml(markdown); System.out.println(html); }
测试结果如下:
<p>| 列1 | 列2 | | ----- | ----- | | 数据1 | 数据2 |</p>
没有将表格转换为html table标签, 所以需要启用表格扩展, 如下:
MutableDataSet options = new MutableDataSet(); // 启用表格扩展,支持 Markdown 表格语法 options.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create())); // 禁用跨列 options.set(TablesExtension.COLUMN_SPANS, false); // 表头固定为 1 行 options.set(TablesExtension.MIN_HEADER_ROWS, 1); options.set(TablesExtension.MAX_HEADER_ROWS, 1); // 自动补全缺失列、丢弃多余列 options.set(TablesExtension.APPEND_MISSING_COLUMNS, true); options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true);
测试结果如下:
<table> <thead> <tr><th>列1</th><th>列2</th></tr> </thead> <tbody> <tr><td>数据1</td><td>数据2</td></tr> </tbody> </table>
5.2 标签属性扩展
flexmark支持对标签属性的操作, 需要实现其AttributeProviderFactory类, 比如给对应标签添加class属性, 如下:
HtmlRenderer renderer = HtmlRenderer.builder(options) .attributeProviderFactory(new IndependentAttributeProviderFactory() { @Override public @NotNull AttributeProvider apply(@NotNull LinkResolverContext context) { return (node, part, attributes) -> { // 标题 if (node instanceof Heading) { Heading heading = (Heading) node; attributes.addValue("class", "heading" + heading.getLevel()); } // 正文 if (node instanceof Text) { attributes.addValue("class", "Normal"); } // 段落 if (node instanceof Paragraph) { attributes.addValue("class", "paragraph"); } // 无序列表 if (node instanceof BulletList) { attributes.addValue("class", "bulletList"); } // 有序列表 if (node instanceof OrderedList) { attributes.addValue("class", "bulletList"); } // 表格 if (node instanceof TableBlock) { attributes.addValue("class", "tableBlock"); } }; } }) .build();
测试如下内容:
public static void main(String[] args) { String markdown = "## 嘉文四世n" + "n" + "> 德玛西亚n" + "n" + "**给我找些更强的敌人!**n" + "n" + "| 列1 | 列2 |n" + "| ----- | ----- |n" + "| 数据1 | 数据2 |"; final String html = convertMarkdownToHtml(markdown); System.out.println(html); }
测试结果如下:
<h2 class="heading2">嘉文四世</h2> <blockquote> <p class="paragraph">德玛西亚</p> </blockquote> <p class="paragraph"><strong>给我找些更强的敌人!</strong></p> <table class="tableBlock"> <thead> <tr><th>列1</th><th>列2</th></tr> </thead> <tbody> <tr><td>数据1</td><td>数据2</td></tr> </tbody> </table>
5.3 完善Html结构
上述的测试结果中输出的都是markdown语句翻译后的html代码块, 并不是一个完整的html页面内容, 比如要将结果输出成html文件并展示的话还需要html完整的骨架标签如:<html><body>等, 这时候就需要使用jsoup进行优化
-
添加对应的坐标
<jsoup.version>1.17.2</jsoup.version> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>${jsoup.version}</version> </dependency> -
完善html结构
public static String wrapperHtml(String htmlContent) { org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(htmlContent); jsoupDoc.outputSettings() // 内容输出时遵循XML语法规则 .syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml) // 内容转义时遵循xhtml规范 .escapeMode(Entities.EscapeMode.xhtml) // 禁用格式化输出 .prettyPrint(false); return jsoupDoc.html(); } public static void main(String[] args) { String markdown = "## 嘉文四世n" + "n" + "> 德玛西亚n" + "n" + "**给我找些更强的敌人!**n" + "n" + "| 列1 | 列2 |n" + "| ----- | ----- |n" + "| 数据1 | 数据2 |"; final String html = convertMarkdownToHtml(markdown); final String wrappedHtml = wrapperHtml(html); System.out.println(wrappedHtml); }测试结果如下:
<html><head></head><body><h2 class="heading2">嘉文四世</h2> <blockquote> <p class="paragraph">德玛西亚</p> </blockquote> <p class="paragraph"><strong>给我找些更强的敌人!</strong></p> <table class="tableBlock"> <thead> <tr><th>列1</th><th>列2</th></tr> </thead> <tbody> <tr><td>数据1</td><td>数据2</td></tr> </tbody> </table> </body></html>
6. 完整测试代码
package md2html; import com.vladsch.flexmark.ast.BulletList; import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.ast.OrderedList; import com.vladsch.flexmark.ast.Paragraph; import com.vladsch.flexmark.ast.Text; import com.vladsch.flexmark.ext.tables.TableBlock; import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.html.AttributeProvider; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.html.IndependentAttributeProviderFactory; import com.vladsch.flexmark.html.renderer.LinkResolverContext; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.data.MutableDataSet; import org.jetbrains.annotations.NotNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Entities; import java.util.Collections; public class MarkdownToHtml { public static String convertMarkdownToHtml(String markdown) { // 创建配置集 MutableDataSet options = new MutableDataSet(); // 启用表格扩展,支持 Markdown 表格语法 options.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create())); // 禁用跨列 options.set(TablesExtension.COLUMN_SPANS, false); // 表头固定为 1 行 options.set(TablesExtension.MIN_HEADER_ROWS, 1); options.set(TablesExtension.MAX_HEADER_ROWS, 1); // 自动补全缺失列、丢弃多余列 options.set(TablesExtension.APPEND_MISSING_COLUMNS, true); options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true); // 创建解析器和渲染器 Parser parser = Parser.builder(options) .build(); HtmlRenderer renderer = HtmlRenderer.builder(options) .attributeProviderFactory(new IndependentAttributeProviderFactory() { @Override public @NotNull AttributeProvider apply(@NotNull LinkResolverContext context) { return (node, part, attributes) -> { // 标题 if (node instanceof Heading) { Heading heading = (Heading) node; attributes.addValue("class", "heading" + heading.getLevel()); } // 正文 if (node instanceof Text) { attributes.addValue("class", "Normal"); } // 段落 if (node instanceof Paragraph) { attributes.addValue("class", "paragraph"); } // 无序列表 if (node instanceof BulletList) { attributes.addValue("class", "bulletList"); } // 有序列表 if (node instanceof OrderedList) { attributes.addValue("class", "bulletList"); } // 表格 if (node instanceof TableBlock) { attributes.addValue("class", "tableBlock"); } }; } }) .build(); // 解析 Markdown 文本 Node document = parser.parse(markdown); // 渲染为 HTML return renderer.render(document); } public static String wrapperHtml(String htmlContent) { org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(htmlContent); jsoupDoc.outputSettings() // 内容输出时遵循XML语法规则 .syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml) // 内容转义时遵循xhtml规范 .escapeMode(Entities.EscapeMode.xhtml) // 禁用格式化输出 .prettyPrint(false); return jsoupDoc.html(); } public static void main(String[] args) { String markdown = "## 嘉文四世n" + "n" + "> 德玛西亚n" + "n" + "**给我找些更强的敌人!**n" + "n" + "| 列1 | 列2 |n" + "| ----- | ----- |n" + "| 数据1 | 数据2 |"; final String html = convertMarkdownToHtml(markdown); final String wrappedHtml = wrapperHtml(html); System.out.println(wrappedHtml); } }
7. 封装工具类
为了更方便的使用flexmark, 我将其常用的方法封装成链式调用的工具类, 内容如下:
import com.vladsch.flexmark.ast.BlockQuote; import com.vladsch.flexmark.ast.BulletList; import com.vladsch.flexmark.ast.Code; import com.vladsch.flexmark.ast.Emphasis; import com.vladsch.flexmark.ast.FencedCodeBlock; import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.ast.Image; import com.vladsch.flexmark.ast.IndentedCodeBlock; import com.vladsch.flexmark.ast.Link; import com.vladsch.flexmark.ast.ListItem; import com.vladsch.flexmark.ast.OrderedList; import com.vladsch.flexmark.ast.Paragraph; import com.vladsch.flexmark.ast.StrongEmphasis; import com.vladsch.flexmark.ast.ThematicBreak; import com.vladsch.flexmark.ext.tables.TableBlock; import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.html.AttributeProvider; import com.vladsch.flexmark.html.AttributeProviderFactory; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.html.IndependentAttributeProviderFactory; import com.vladsch.flexmark.html.renderer.LinkResolverContext; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Document; import com.vladsch.flexmark.util.ast.Node; import com.vladsch.flexmark.util.data.MutableDataSet; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Entities; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collections; /** * markdown 工具类 * * @author ludangxin * @since 2025/10/14 */ @Slf4j public class Markdowns { public static MarkdownBuilder builder(InputStream inputStream, String charset) { String markdownContent = readMarkdownContent(inputStream, charset); return builder(markdownContent); } public static MarkdownBuilder builder(InputStream inputStream) { String markdownContent = readMarkdownContent(inputStream); return builder(markdownContent); } public static MarkdownBuilder builder(File file) { String markdownContent = readMarkdownContent(file); return builder(markdownContent); } public static MarkdownBuilder builder(String markdownContent) { return new MarkdownBuilder().content(markdownContent); } public static String readMarkdownContent(File file) { if (file == null || !file.exists()) { return ""; } try { return readMarkdownContent(new FileReader(file)); } catch (Exception e) { log.error("failed to read markdown content", e); } return ""; } public static String readMarkdownContent(InputStream inputStream) { try { return readMarkdownContent(new InputStreamReader(inputStream)); } catch (Exception e) { log.error("failed to read markdown content", e); } return ""; } public static String readMarkdownContent(InputStream inputStream, String charset) { if (charset == null || charset.isEmpty()) { return readMarkdownContent(new InputStreamReader(inputStream)); } try { return readMarkdownContent(new InputStreamReader(inputStream, charset)); } catch (Exception e) { log.error("failed to read markdown content", e); } return ""; } public static String readMarkdownContent(InputStreamReader inputStreamReader) { try (BufferedReader reader = new BufferedReader(inputStreamReader)) { StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); sb.append(System.lineSeparator()); } return sb.toString(); } catch (IOException e) { log.error("failed to read markdown content", e); } return ""; } public static class MarkdownBuilder { private String content; private MutableDataSet options; private AttributeProviderFactory attributeProviderFactory; private AttributeProvider attributeProvider; private MarkdownBuilder content(String content) { this.content = content; return this; } public MarkdownBuilder options(MutableDataSet options) { this.options = options; return this; } public MarkdownBuilder attributeProviderFactory(AttributeProviderFactory attributeProviderFactory) { this.attributeProviderFactory = attributeProviderFactory; return this; } public MarkdownBuilder attributeProvider(AttributeProvider attributeProvider) { this.attributeProvider = attributeProvider; return this; } public MarkdownBuilder printContent() { System.out.println(content); return this; } public boolean isMarkdown() { if (content == null || content.trim() .isEmpty()) { return false; } final Document document = this.buildDocument(); return hasMarkdownNodes(document); } public Document buildDocument() { Parser parser = Parser.builder(this.getOptionsOrDefault()) .build(); return parser.parse(content); } public String buildHtmlContent() { return this.wrapperHtml(this.getHtmlRenderer() .render(this.buildDocument())); } public String buildRawHtmlContent() { return this.getHtmlRenderer() .render(this.buildDocument()); } public String buildRawHtmlIfMarkdown() { if (this.isMarkdown()) { return this.buildRawHtmlContent(); } return content; } public String buildHtmlIfMarkdown() { if (this.isMarkdown()) { return this.buildHtmlContent(); } return content; } private HtmlRenderer getHtmlRenderer() { final HtmlRenderer.Builder builder = HtmlRenderer.builder(getOptionsOrDefault()); if (attributeProviderFactory != null) { builder.attributeProviderFactory(attributeProviderFactory); } if (attributeProviderFactory == null && attributeProvider != null) { final IndependentAttributeProviderFactory independentAttributeProviderFactory = new IndependentAttributeProviderFactory() { @Override public @NotNull AttributeProvider apply(@NotNull LinkResolverContext linkResolverContext) { return attributeProvider; } }; builder.attributeProviderFactory(independentAttributeProviderFactory); } return builder.build(); } private MutableDataSet getOptionsOrDefault() { if (options == null) { return this.defaultOptions(); } else { return options; } } private MutableDataSet defaultOptions() { MutableDataSet options = new MutableDataSet(); // 启用表格扩展,支持 Markdown 表格语法 options.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create())); // 禁用跨列 options.set(TablesExtension.COLUMN_SPANS, false); // 表头固定为 1 行 options.set(TablesExtension.MIN_HEADER_ROWS, 1); options.set(TablesExtension.MAX_HEADER_ROWS, 1); // 自动补全缺失列、丢弃多余列 options.set(TablesExtension.APPEND_MISSING_COLUMNS, true); options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true); return options; } private String wrapperHtml(String htmlContent) { org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(htmlContent); jsoupDoc.outputSettings() // 内容输出时遵循XML语法规则 .syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml) // 内容转义时遵循xhtml规范 .escapeMode(Entities.EscapeMode.xhtml) // 禁用格式化输出 .prettyPrint(false); return jsoupDoc.html(); } /** * 检查 AST 中是否存在 Markdown 特有节点(非纯文本段落) */ private static boolean hasMarkdownNodes(Node node) { if (node == null) { return false; } // 判断当前节点是否为 Markdown 特有节点(非纯文本) if (isMarkdownSpecificNode(node)) { return true; } // 递归检查子节点 Node child = node.getFirstChild(); while (child != null) { if (hasMarkdownNodes(child)) { return true; } child = child.getNext(); } return false; } /** * 判定节点是否为 Markdown 特有节点(非纯文本段落) * 纯文本段落(Paragraph)且无任何格式(如链接、粗体等)则视为非 Markdown */ private static boolean isMarkdownSpecificNode(Node node) { // 标题(# 标题) if (node instanceof Heading) { return true; } // 列表(有序/无序) if (node instanceof BulletList || node instanceof OrderedList) { return true; } // 列表项 if (node instanceof ListItem) { return true; } // 链接([文本](url)) if (node instanceof Link) { return true; } // 图片() if (node instanceof Image) { return true; } // 粗体(**文本** 或 __文本__) if (node instanceof StrongEmphasis) { return true; } // 斜体(*文本* 或 _文本_) if (node instanceof Emphasis) { return true; } // 代码块(```代码```) if (node instanceof FencedCodeBlock || node instanceof IndentedCodeBlock) { return true; } // 表格(| 表头 | ... |) if (node instanceof TableBlock) { return true; } // 引用(> 引用内容) if (node instanceof BlockQuote) { return true; } // 水平线(--- 或 ***) if (node instanceof ThematicBreak) { return true; } // 段落节点需进一步检查是否包含 inline 格式(如粗体、链接等) if (node instanceof Paragraph) { return hasInlineMarkdownNodes(node); } // 其他节点(如文本节点)视为非特有 return false; } /** * 检查段落中是否包含 inline 格式(如粗体、链接等) */ private static boolean hasInlineMarkdownNodes(Node paragraph) { Node child = paragraph.getFirstChild(); while (child != null) { // 若段落中包含任何 Markdown inline 节点,则视为 Markdown if (child instanceof Link || child instanceof Image || child instanceof StrongEmphasis || child instanceof Emphasis || child instanceof Code) { return true; } child = child.getNext(); } return false; } } }
8. 测试示例
import com.vladsch.flexmark.ast.BulletList; import com.vladsch.flexmark.ast.Heading; import com.vladsch.flexmark.ast.OrderedList; import com.vladsch.flexmark.ast.Paragraph; import com.vladsch.flexmark.ast.Text; import com.vladsch.flexmark.ext.tables.TableBlock; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; /** * 测试工具类 * * @author ludangxin * @since 2025/10/14 */ @Slf4j public class Md2htmlTest { @Test public void given_md_str_then_print_complete_html() { final String html = Markdowns.builder("# 简介 n hello world~") // 打印md内容 .printContent() // 构建html内容, 自动完善html结构 .buildHtmlContent(); log.info(html); // # 简介 // hello world~ //[main] INFO md2html.Md2htmlTest -- <html><head></head><body><h1>简介</h1> //<p>hello world~</p> //</body></html> } @Test public void given_md_str_then_print_raw_html() { final String html = Markdowns.builder("# 简介 n hello world~") // 构建raw html内容 .buildRawHtmlContent(); log.info(html); //[main] INFO md2html.Md2htmlTest -- <h1>简介</h1> //<p>hello world~</p> } @Test public void given_md_file_then_print_raw_html() { final String html = Markdowns.builder(new File("src/test/resources/test.md")) // 构建raw html内容 .buildRawHtmlContent(); log.info(html); //[main] INFO md2html.Md2htmlTest -- <h2>嘉文四世</h2> //<blockquote> //<p>德玛西亚</p> //</blockquote> //<p><strong>给我找些更强的敌人!</strong></p> //<table> //<thead> //<tr><th>列1</th><th>列2</th></tr> //</thead> //<tbody> //<tr><td>数据1</td><td>数据2</td></tr> //</tbody> //</table> } @Test @SneakyThrows public void given_md_stream_then_print_complete_html() { final InputStream fileInputStream = Files.newInputStream(Paths.get("src/test/resources/test.md")); final String html = Markdowns.builder(fileInputStream) // 构建html内容 .buildHtmlContent(); log.info(html); //[main] INFO md2html.Md2htmlTest -- <html><head></head><body><h2>嘉文四世</h2> //<blockquote> //<p>德玛西亚</p> //</blockquote> //<p><strong>给我找些更强的敌人!</strong></p> //<table> //<thead> //<tr><th>列1</th><th>列2</th></tr> //</thead> //<tbody> //<tr><td>数据1</td><td>数据2</td></tr> //</tbody> //</table> //</body></html> } @Test public void given_non_md_content_then_print_complete_html() { // 输入非markdown语法的内容 final String html = Markdowns.builder("hello world~") // 构建html内容 (如果内容是md语法则转换为html, 如不不是 则原样输出) .buildHtmlIfMarkdown(); // 输入非markdown语法的内容 final String html2 = Markdowns.builder("## hello world~") // 构建html内容 (如果内容是md语法则转换为html, 如不不是 则原样输出) .buildHtmlIfMarkdown(); log.info(html); //[main] INFO md2html.Md2htmlTest -- hello world~ log.info(html2); //[main] INFO md2html.Md2htmlTest -- <html><head></head><body><h2>hello world~</h2> } @Test @SneakyThrows public void given_md_stream_and_attr_provider_then_print_raw_html() { final InputStream fileInputStream = Files.newInputStream(Paths.get("src/test/resources/test.md")); final String html = Markdowns.builder(fileInputStream) .attributeProvider((node, attributablePart, attributes) -> { // 标题 if (node instanceof Heading) { Heading heading = (Heading) node; attributes.addValue("class", "heading" + heading.getLevel()); } // 正文 if (node instanceof Text) { attributes.addValue("class", "Normal"); } // 段落 if (node instanceof Paragraph) { attributes.addValue("class", "paragraph"); } // 无序列表 if (node instanceof BulletList) { attributes.addValue("class", "bulletList"); } // 有序列表 if (node instanceof OrderedList) { attributes.addValue("class", "bulletList"); } // 表格 if (node instanceof TableBlock) { attributes.addValue("class", "tableBlock"); } }) .buildRawHtmlContent(); log.info(html); //[main] INFO md2html.Md2htmlTest -- <h2 class="heading2">嘉文四世</h2> //<blockquote> //<p class="paragraph">德玛西亚</p> //</blockquote> //<p class="paragraph"><strong>给我找些更强的敌人!</strong></p> //<table class="tableBlock"> //<thead> //<tr><th>列1</th><th>列2</th></tr> //</thead> //<tbody> //<tr><td>数据1</td><td>数据2</td></tr> //</tbody> //</table> } }
9. 小节
本章使用flexmark将markdown内容转换为html内容, 并介绍了其高级的配置功能和使用jsoup完善html结构,最后封装链式调用的工具类和对应的单元测试代码, 能够方便的将各种形式的markdown内容转换为html内容, 下一章将介绍将html转换为word内容
10. 源码
测试过程中的代码已全部上传至github, 欢迎点赞收藏 仓库地址: https://github.com/ludangxin/markdown2html