前一篇写的是markdown格式的文本内容转换保存为word文档,是假定已经有一个现成的markdown格式的文本,然后直接转换保存为word文档,不过在开发中,通常情况下,数据是从数据库中获取,拿到的数据映射到java对象上,这一篇就是处理如何将java对象数据生成为markdown文本。
添加Maven依赖:
<!-- excel工具 练习的项目自身的依赖-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 新添加的依赖-->
<!-- markdown格式转换为html -->
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.21.0</version>
</dependency>
<!-- poi-tl和poi-tl-plugin-markdown是处理markdown格式转换为word格式,处理只处理markdown转换为html,只需要commonnark依赖即可-->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.10.1</version>
</dependency>
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl-plugin-markdown</artifactId>
<version>1.0.3</version>
</dependency>
1.首先编写一个markdown的语法生成的处理类:
package com.xiaomifeng1010.common.markdown;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2024-09-21 20:50
* @Description
*/
public class MarkdownHandler {
// ~ APIs
// -----------------------------------------------------------------------------------------------------------------
public static SectionBuilder of() {
return new SectionBuilder(new Section(Section.Type.NORMAL, null, null, null, 0));
}
// ~ public classes & public constants & public enums
// -----------------------------------------------------------------------------------------------------------------
public enum Style {
NORMAL("normal"), BOLD("bold"), ITALIC("italic"),
RED("red"), GREEN("green"), GRAY("gray"), YELLOW("gold"), BLUE("blue");
private final String name;
Style(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static class Fonts {
public static final Fonts EMPTY = Fonts.of("");
private final String text;
// ~ private fields
// -------------------------------------------------------------------------------------------------------------
private Set<Style> styles = Collections.emptySet();
private Fonts(String text, Style... style) {
this.text = text != null ? text : "";
if (style != null) {
this.styles = new HashSet<>(Arrays.asList(style));
}
}
// ~ public methods
// -------------------------------------------------------------------------------------------------------------
public static Fonts of(String text) {
return new Fonts(text, Style.NORMAL);
}
public static Fonts of(String text, Style... style) {
return new Fonts(text, style);
}
public boolean isEmpty() {
return this.text == null || this.text.isEmpty();
}
@Override
public String toString() {
if (styles.contains(Style.NORMAL)) {
return text;
}
String last = text;
for (Style style : styles) {
last = parseStyle(last, style);
}
return last;
}
// ~ private methods
// -------------------------------------------------------------------------------------------------------------
private String parseStyle(String text, Style style) {
if (text == null || style == null) {
return text;
}
switch (style) {
case NORMAL:
break;
case BOLD:
return "**" + text + "**";
case ITALIC:
return "*" + text + "*";
case RED:
case GREEN:
case BLUE:
case YELLOW:
return "<font color='" + style.getName() + "'>" + text + "</font>";
}
return text;
}
}
/**
* 代表一行,可以是一个普通文本或一个K-V(s)数据
*/
public static class MetaData {
// ~ public constants
// -------------------------------------------------------------------------------------------------------------
public static final String DEFAULT_SEPARATOR = ":";
public static final String DEFAULT_VALUE_SEPARATOR = " | ";
public static final String LINK_TEMPLATE = "[%s▸](%s)";
// ~ private fields
// -------------------------------------------------------------------------------------------------------------
private final Type type;
private final Fonts text;
private final Collection<Fonts> values;
private final String separator = DEFAULT_SEPARATOR;
private final String valueSeparator = DEFAULT_VALUE_SEPARATOR;
public MetaData(Fonts text) {
this(text, null);
}
public MetaData(Type type) {
this(type, null, null);
}
public MetaData(Fonts text, Collection<Fonts> values) {
this(Type.NORMAL, text, values);
}
public MetaData(Type type, Fonts text, Collection<Fonts> values) {
this.type = type;
this.text = text;
this.values = values;
}
@Override
public String toString() {
return generateString(this.valueSeparator);
}
/**
* generate one line
*/
private String generateString(String valueSeparator) {
boolean hasValues = values != null && !values.isEmpty();
boolean hasText = text != null && !text.isEmpty();
StringJoiner joiner = new StringJoiner(valueSeparator);
String ret = "";
switch (type) {
case NORMAL:
if (hasText && hasValues) {
values.forEach(v -> joiner.add(v.toString()));
ret = text + separator + joiner;
} else if (!hasText && hasValues) {
values.forEach(v -> joiner.add(v.toString()));
ret = joiner.toString();
} else if (hasText) {
ret = text.toString();
}
break;
case LINK:
if (hasText && hasValues) {
Fonts fonts = values.stream().findFirst().orElse(null);
if (fonts == null) {
break;
}
ret = String.format(LINK_TEMPLATE, text, fonts);
} else if (!hasText && hasValues) {
Fonts url = values.stream().findFirst().orElse(null);
if (url == null) {
break;
}
ret = String.format(LINK_TEMPLATE, url, url);
} else if (hasText) {
ret = String.format(LINK_TEMPLATE, text, text);
}
break;
case LINK_LIST:
if (hasText && hasValues) {
ret = text + separator + generateLinkList(values);
} else if (!hasText && hasValues) {
ret = generateLinkList(values);
} else if (hasText) {
ret = String.format(LINK_TEMPLATE, text, text);
}
break;
case BR:
ret = "<br>";
}
return ret;
}
// ~ private methods
// -------------------------------------------------------------------------------------------------------------
private String generateLinkList(Collection<Fonts> values) {
if (values == null || values.isEmpty()) {
return "";
}
Object[] valueArr = values.toArray();
StringJoiner linkList = new StringJoiner(valueSeparator);
for (int i = 0; i + 1 < valueArr.length; i += 2) {
linkList.add(String.format(LINK_TEMPLATE, valueArr[i], valueArr[i + 1]));
}
boolean isPairNum = (valueArr.length % 2) == 0;
if (!isPairNum) {
String lastUrl = valueArr[valueArr.length - 1].toString();
linkList.add(String.format(LINK_TEMPLATE, lastUrl, lastUrl));
}
return linkList.toString();
}
private enum Type {
/** only plain text, plain text list with a name */
NORMAL,
/**
* text : link name
* values: index 0 is URL if existed.
*/
LINK, LINK_LIST,
BR,
}
}
// ~ private class & private implements
// -----------------------------------------------------------------------------------------------------------------
private static class Section {
private final int depth;
private Type type;
private Object data;
private Section parent;
private List<Section> children;
private Section(Type type, Object data, Section parent, List<Section> children, int depth) {
this.type = type;
this.data = data;
this.parent = parent;
this.children = children;
this.depth = depth;
}
// ~ public methods
// -------------------------------------------------------------------------------------------------------------
public void addChild(Section child) {
lazyInitChildren();
children.add(child);
}
public boolean childIsEmpty() {
return children == null || children.isEmpty();
}
// ~ private methods
// -------------------------------------------------------------------------------------------------------------
private StringBuilder parse(StringBuilder latestData) {
switch (type) {
case LINK:
case NORMAL:
latestData.append('\n').append(parseData(""));
return latestData;
case BIG_TITLE:
latestData.append('\n').append(parseData("# "));
return latestData;
case TITLE:
latestData.append('\n').append(parseData("##### "));
return latestData;
case SUBTITLE:
latestData.append('\n').append(parseData("### "));
return latestData;
case REF:
return parseRefSection(latestData);
case CODE:
StringBuilder codeBlock = new StringBuilder(latestData.length() + 10);
codeBlock.append("\n```").append(latestData).append("\n```");
return codeBlock;
case ORDER_LIST:
return parseOrderListSection(latestData);
case UN_ORDER_LIST:
return parseUnOrderListSection(latestData);
case TABLE:
return parseTableSection(latestData);
case BR:
return latestData.append(parseData(""));
}
return latestData;
}
private String parseData(String prefix) {
if (data == null) {
return "";
}
return prefix + data;
}
private StringBuilder parseRefSection(StringBuilder latestData) {
char[] chars = latestData.toString().toCharArray();
if (chars.length <= 0) {
return latestData;
}
StringBuilder data = new StringBuilder(chars.length * 2);
if (chars[0] != '\n') {
data.append("> ");
}
char last = 0;
for (char c : chars) {
if (last == '\n') {
data.append("> ");
}
data.append(c);
last = c;
}
return data;
}
private StringBuilder parseOrderListSection(StringBuilder latestData) {
char[] chars = latestData.toString().toCharArray();
if (chars.length <= 0) {
return latestData;
}
StringBuilder data = new StringBuilder(chars.length * 2);
String padding = String.join("", Collections.nCopies(depth * 4, " "));
int order = 1;
if (chars[0] != '\n') {
data.append(padding).append(order++).append(". ");
}
char last = 0;
for (char c : chars) {
if (last == '\n' && c != '\n' && c != ' ') {
data.append(padding).append(order++).append(". ");
}
data.append(c);
last = c;
}
return data;
}
private StringBuilder parseUnOrderListSection(StringBuilder latestData) {
char[] chars = latestData.toString().toCharArray();
if (chars.length <= 0) {
return latestData;
}
StringBuilder data = new StringBuilder(chars.length * 2);
String padding = String.join("", Collections.nCopies(depth * 4, " "));
if (chars[0] != '\n') {
data.append(padding).append("- ");
}
char last = 0;
for (char c : chars) {
if (last == '\n' && c != '\n' && c != ' ') {
data.append(padding).append("- ");
}
data.append(c);
last = c;
}
return data;
}
private StringBuilder parseTableSection(StringBuilder latestData) {
if (data != null) {
Object[][] tableData = (Object[][]) data;
if (tableData.length > 0 && tableData[0].length > 0) {
StringJoiner titles = new StringJoiner(" | "), extras = new StringJoiner(" | ");
for (Object t : tableData[0]) {
titles.add(t != null ? t.toString() : "");
extras.add("-");
}
latestData.append("\n\n").append(titles).append('\n').append(extras);
for (int i = 1; i < tableData.length; i++) {
StringJoiner dataJoiner = new StringJoiner(" | ");
for (int j = 0; j < tableData[i].length; j++) {
dataJoiner.add(tableData[i][j] != null ? tableData[i][j].toString() : "");
}
latestData.append('\n').append(dataJoiner);
}
}
}
return latestData.append('\n');
}
private void lazyInitChildren() {
if (children == null) {
children = new ArrayList<>();
}
}
// ~ getter & setter
// -------------------------------------------------------------------------------------------------------------
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Section getParent() {
return parent;
}
public void setParent(Section parent) {
this.parent = parent;
}
public List<Section> getChildren() {
return children;
}
public void setChildren(List<Section> children) {
this.children = children;
}
public int getDepth() {
return depth;
}
private enum Type {
/**
* data is {@link MetaData} and plain text
*/
NORMAL,
/**
* data is {@link MetaData} and h2
*/
BIG_TITLE,
/**
* data is {@link MetaData} and h3
*/
TITLE,
/**
* data is {@link MetaData} and h4
*/
SUBTITLE,
/**
* data is {@code null}, content is children
*/
REF,
/**
* data is {@code null}, content is children
*/
CODE,
/**
* data is matrix, aka String[][]
*/
TABLE,
/**
* data is {@code null}, content is children
*/
ORDER_LIST,
/**
* data is {@code null}, content is children
*/
UN_ORDER_LIST,
/**
* data is {@link MetaData}
*/
LINK,
BR
}
}
public static class SectionBuilder {
private static final MdParser parser = new MdParser();
/**
* first is root
*/
private final Section curSec;
/**
* code, ref curr -> par
*/
private Section parentSec;
/**
* init null
*/
private SectionBuilder parentBuilder;
private SectionBuilder(Section curSec) {
this.curSec = curSec;
}
private SectionBuilder(Section curSec, Section parentSec, SectionBuilder parentBuilder) {
this.curSec = curSec;
this.parentSec = parentSec;
this.parentBuilder = parentBuilder;
}
// ~ public methods
// -------------------------------------------------------------------------------------------------------------
public SectionBuilder text(String text) {
return text(text, (String) null);
}
public SectionBuilder text(String name, String value) {
if (name != null) {
Collection<Fonts> values
= value != null ? Collections.singletonList(Fonts.of(value)) : Collections.emptyList();
curSec.addChild(new Section(Section.Type.NORMAL,
new MetaData(MetaData.Type.NORMAL, Fonts.of(name, (Style) null), values),
curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder text(String text, Style... style) {
if (text != null) {
curSec.addChild(new Section(Section.Type.NORMAL, new MetaData(Fonts.of(text, style)), curSec,
null, curSec.getDepth()));
}
return this;
}
public SectionBuilder text(Collection<String> values) {
if (values != null && !values.isEmpty()) {
text(null, values);
}
return this;
}
public SectionBuilder text(String name, Collection<String> values) {
if (values == null || values.size() <= 0) {
return text(name);
}
return text(name, null, values);
}
public SectionBuilder text(String name, Style valueStyle, Collection<String> values) {
if (values == null || values.size() <= 0) {
return text(name);
}
if (valueStyle == null) {
valueStyle = Style.NORMAL;
}
List<Fonts> ele = new ArrayList<>(values.size());
for (String value : values) {
ele.add(Fonts.of(value, valueStyle));
}
curSec.addChild(new Section(Section.Type.NORMAL, new MetaData(Fonts.of(name), ele), curSec, null,
curSec.getDepth()));
return this;
}
public SectionBuilder bigTitle(String title) {
if (StringUtils.isNotBlank(title)) {
curSec.addChild(new Section(Section.Type.BIG_TITLE, new MetaData(Fonts.of(title)), curSec,
null, curSec.getDepth()));
}
return this;
}
public SectionBuilder title(String title) {
return title(title, Style.NORMAL);
}
public SectionBuilder title(String title, Style color) {
if (StringUtils.isNotBlank(title)) {
curSec.addChild(new Section(Section.Type.TITLE, new MetaData(Fonts.of(title, color)),
curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder title(String title, Fonts... label) {
return title(title, null, label);
}
public SectionBuilder title(String title, Style titleColor, Fonts... label) {
if (StringUtils.isNotBlank(title)) {
if (titleColor == null) {
titleColor = Style.NORMAL;
}
List<Fonts> labelList = label != null ? Arrays.asList(label) : Collections.emptyList();
curSec.addChild(new Section(Section.Type.TITLE, new MetaData(Fonts.of(title, titleColor), labelList),
curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder subTitle(String title) {
if (StringUtils.isNotBlank(title)) {
curSec.addChild(new Section(Section.Type.SUBTITLE, new MetaData(Fonts.of(title)),
curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder ref() {
Section refSection = new Section(Section.Type.REF, null, curSec, new ArrayList<>(), curSec.getDepth());
curSec.addChild(refSection);
return new SectionBuilder(refSection, curSec, this);
}
public SectionBuilder endRef() {
return this.parentBuilder != null ? this.parentBuilder : this;
}
public TableDataBuilder table() {
return new TableDataBuilder(curSec, this);
}
public SectionBuilder link(String url) {
return link(null, url);
}
public SectionBuilder link(String name, String url) {
if (StringUtils.isBlank(name)) {
name = url;
}
if (StringUtils.isNotBlank(url)) {
MetaData links = new MetaData(MetaData.Type.LINK, Fonts.of(name),
Collections.singletonList(Fonts.of(url)));
curSec.addChild(new Section(Section.Type.NORMAL, links, curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder links(Map<String, String> urlMappings) {
return links(null, urlMappings);
}
public SectionBuilder links(String name, Map<String, String> urlMappings) {
if (urlMappings != null && !urlMappings.isEmpty()) {
List<Fonts> serialUrlInfos = new ArrayList<>();
for (Map.Entry<String, String> entry : urlMappings.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
serialUrlInfos.add(Fonts.of(key != null ? key : ""));
serialUrlInfos.add(Fonts.of(value != null ? value : ""));
}
Fonts wrappedName = StringUtils.isNotBlank(name) ? Fonts.of(name) : Fonts.EMPTY;
MetaData linksGroup = new MetaData(MetaData.Type.LINK_LIST, wrappedName, serialUrlInfos);
curSec.addChild(new Section(Section.Type.NORMAL, linksGroup, curSec, null, curSec.getDepth()));
}
return this;
}
public SectionBuilder ol() {
int depth = (curSec.getType() == Section.Type.ORDER_LIST || curSec.getType() == Section.Type.UN_ORDER_LIST)
? curSec.getDepth() + 1
: curSec.getDepth();
Section OrderListSec = new Section(Section.Type.ORDER_LIST, null, curSec, new ArrayList<>(), depth);
curSec.addChild(OrderListSec);
return new SectionBuilder(OrderListSec, curSec, this);
}
public SectionBuilder endOl() {
return this.parentBuilder != null ? this.parentBuilder : this;
}
public SectionBuilder ul() {
int depth = (curSec.getType() == Section.Type.ORDER_LIST || curSec.getType() == Section.Type.UN_ORDER_LIST)
? curSec.getDepth() + 1
: curSec.getDepth();
Section unOrderListSec = new Section(Section.Type.UN_ORDER_LIST, null, curSec, new ArrayList<>(), depth);
curSec.addChild(unOrderListSec);
return new SectionBuilder(unOrderListSec, curSec, this);
}
public SectionBuilder endUl() {
return this.parentBuilder != null ? this.parentBuilder : this;
}
public SectionBuilder code() {
Section codeSec = new Section(Section.Type.CODE, null, curSec, new ArrayList<>(), curSec.getDepth());
curSec.addChild(codeSec);
return new SectionBuilder(codeSec, curSec, this);
}
public SectionBuilder endCode() {
return this.parentBuilder != null ? this.parentBuilder : this;
}
public SectionBuilder br() {
curSec.addChild(new Section(Section.Type.BR, new MetaData(MetaData.Type.BR), parentSec, null,
curSec.getDepth()));
return this;
}
public String build() {
return parser.parse(curSec);
}
}
public static class TableDataBuilder {
private final Section parentSec;
private final SectionBuilder parentBuilder;
private Object[][] tableData;
private TableDataBuilder(Section parentSec, SectionBuilder parentBuilder) {
this.parentSec = parentSec;
this.parentBuilder = parentBuilder;
}
// ~ public methods
// -------------------------------------------------------------------------------------------------------------
public TableDataBuilder data(Object[][] table) {
if (table != null && table.length > 0 && table[0].length > 0) {
tableData = table;
}
return this;
}
public TableDataBuilder data(Object[] title, Object[][] data) {
if (title == null && data != null) {
return data(data);
}
if (data != null && data.length > 0 && data[0].length > 0) {
int minCol = Math.min(title.length, data[0].length);
tableData = new Object[data.length + 1][minCol];
tableData[0] = Arrays.copyOfRange(title, 0, minCol);
for (int i = 0; i < data.length; i++) {
tableData[i + 1] = Arrays.copyOfRange(data[i], 0, minCol);
}
}
return this;
}
public SectionBuilder endTable() {
parentSec.addChild(new Section(Section.Type.TABLE, tableData, parentSec, null, parentSec.getDepth()));
return parentBuilder;
}
}
private static class MdParser {
// ~ public methods
// -------------------------------------------------------------------------------------------------------------
public String parse(Section sec) {
Section root = findRoot(sec);
return doParse(root, root).toString().trim();
}
// ~ private methods
// -------------------------------------------------------------------------------------------------------------
private Section findRoot(Section sec) {
if (sec.getParent() == null) {
return sec;
}
return findRoot(sec.getParent());
}
private StringBuilder doParse(Section cur, Section root) {
if (cur == null) {
return null;
}
if (cur.childIsEmpty()) {
return cur.parse(new StringBuilder());
}
StringBuilder childData = new StringBuilder();
for (Section child : cur.getChildren()) {
StringBuilder part = doParse(child, root);
if (part != null) {
childData.append(part);
}
}
return cur.parse(childData).append(cur.getParent() == root ? '\n' : "");
}
}
}
有了通用的markdown语法生成处理类,则可以根据项目要求,再次封装需要生成的对应的文档中的各类元素对象,比如生成有序列表,无序列表,文档首页标题,副标题,链接,图像,表格等
2. 所以再写一个生成类,里边附带了测试方法
package com.xiaomifeng1010.common.markdown.todoc;
import com.xiaomifeng1010.common.markdown.MarkdownHandler;
import com.xiaomifeng1010.common.utils.MarkdownUtil;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2024-09-27 18:08
* @Description
*/
public class JavaToMarkdownGenerator {
public static void main(String[] args) {
String theWholeMarkdownContent = "";
// 生成一个简单的多元素的word文档
JavaToMarkdownGenerator javaToMarkdownGenerator = new JavaToMarkdownGenerator();
// 首页大标题;注意每部分元素之间一定要换行,不然最后输出到word文档中的时候就会变成带有markdown语法的文本了,
// 比如经济环境分析后边没加"\n\r",那么在word文档中就会变成带有#的目录,会变成 "经济环境分析###目录" 这样子
String title = javaToMarkdownGenerator.generateTitle("经济环境分析")+"\n\r";
// 首页目录
String catalog = javaToMarkdownGenerator.generatecatalog()+"\n\r";
// 插入项目本地的logo图片
// 注意企业项目中不要直接放在resources目录下,因为获取的路径放在markdown的图片类型的语法中(最终是这样:),
// 在转换word时候还是无法通过路径找到图片,linux系统获取文件需要通过流读取,路径的方式会提示找不到文件,windows系统不受影响
// 所以可以将需要插入到markdown中的图片上传到oss中,然后获取oss的图片的完整地址,网络超链接的形式,再插入到markdown中,
// 这样在转换成word时候,会从网络上下载,以流的方式插入到word中
String imgPath = JavaToMarkdownGenerator.class.getResource("/static/canton.jpg").getPath();
String logo = javaToMarkdownGenerator.resloveImg(imgPath, "logo")+"\n\r";
theWholeMarkdownContent = title + catalog + logo;
// 插入正文
List<List<String>> dataList = new ArrayList<>();
// java实践中一般是一个java对象,从数据库查询出来的一个list集合,需要循环获取对象,然后添加到dataList中
// 模拟数据库中查询出来数据
Employee employee = new Employee();
List<Employee> employeeList = employee.getEmployees();
if (CollectionUtils.isNotEmpty(employeeList)) {
for (Employee employee1 : employeeList) {
List<String> list = new ArrayList<>();
list.add(employee1.getName());
list.add(employee1.getSex());
list.add(employee1.getAge());
list.add(employee1.getHeight());
dataList.add(list);
}
String firstTable= javaToMarkdownGenerator.generateTable(dataList, "表格1","姓名", "性别", "芳龄", "身高");
theWholeMarkdownContent=theWholeMarkdownContent + firstTable;
}
// 直接拼接一段富文本,因为网页上新增填写内容的时候,有些参数输入是使用的markdown富文本编辑器
String markdownContent = "\n# 一级标题\n" +
"## 二级标题\n" +
"### 三级标题\n" +
"#### 四级标题\n" +
"##### 五级标题\n" +
"###### 六级标题\n" +
"## 段落\n" +
"这是一段普通的段落。\n" +
"## 列表\n" +
"### 无序列表\n" +
"- 项目1\n" +
"- 项目2\n" +
"- 项目3\n" +
"### 有序列表\n" +
"1. 项目1\n" +
"2. 项目2\n" +
"3. 项目3\n" +
"## 链接\n" +
"[百度](https://www.baidu.com)\n" +
"## 图片\n" +
"\n" +
"## 表格\n" +
"| 表头1 | 表头2 | 表头3 |\n" +
"|-------|-------|-------|\n" +
"| 单元格1 | 单元格2 | 单元格3 |\n" +
"| 单元格4 | 单元格5 | 单元格6 |";
theWholeMarkdownContent=theWholeMarkdownContent+markdownContent;
MarkdownUtil.toDoc(theWholeMarkdownContent,"test228");
System.out.println(catalog);
}
/**
* 使用markdown的有序列表实现生成目录效果
* @return
*/
public String generatecatalog(){
String md = MarkdownHandler.of()
.subTitle("目录")
// 不要加ref方法,可以正常生成markdown文本,但是在转换成word内容的时候,commonmark会报错
// org.commonmark.node.OrderedList cannot be cast to org.commonmark.node.Paragra
// .ol()
.text("文档介绍")
// .endOl()
.ol()
.text("经济环境")
.ol()
.text("1.1 全球化背景")
.text("1.2 通缩问题产生的原因")
.text("1.3 如何应对通缩")
.text("1.4 国家实施的财政政策和货币政策")
.endOl()
.endOl()
.ol()
.text("失业问题")
.ol()
.text("2.1 失业率的概念")
.text("2.2 如何统计失业率")
.text("2.3 如何提升就业率")
.endOl()
.endOl()
.ol()
.text("理财投资")
.ol()
.text("3.1 理财投资的重要性")
.text("3.2 如何选择理财投资产品")
.text("3.3 理财投资的风险管理")
.endOl()
.endOl()
.build();
return md;
}
/**
* 生成表格
* @paramJataList
@paramtableHead
@return
**/
public String generateTable(List<List<String>> datalist, String tableTitle, String... tableHead){
// 添加表头(表格第一行,列标题)
datalist.add( 0, Arrays.asList(tableHead));
String[][] data=datalist.stream().map(list->list.toArray(new String[0])).toArray(String[][]::new);
String markdownContent = MarkdownHandler.of()
.title(tableTitle)
.table()
.data(data)
.endTable()
.build();
return markdownContent;
}
/**
* 文档首页大标题效果
* @param title
* @return
*/
public String generateTitle(String title){
return MarkdownHandler.of().bigTitle(title).build();
}
/**
* 处理图片
* @param imgPath
* @param imgName
* @return
*/
String resloveImg(String imgPath,String imgName){
return "";
}
}
@Data
class Employee{
private String name;
private String sex;
private String age;
// 身高
private String height;
// 体重
private String weight;
// 籍贯
private String nativePlace;
// 职位
private String position;
// 薪资
private String salary;
/**
* 模拟从数据库中查出多条数据
* @return
*/
public List<Employee> getEmployees(){
List<Employee> employees = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Employee employee = new Employee();
employee.setName("张三" + i);
employee.setSex("男");
employee.setAge("18" + i);
employee.setHeight("180" + i);
employee.setWeight("70" + i);
employee.setNativePlace("北京" + i);
employee.setPosition("java开发" + i);
employee.setSalary("10000" + i);
employees.add(employee);
}
return employees;
}
}
测试main方法会调用MarkdownUtil,用于生成word文档保存在本地,或者通过网络进行下载保存。
3.MarkdownUtil工具类:
package com.xiaomifeng1010.common.utils;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.style.*;
import com.deepoove.poi.plugin.markdown.MarkdownRenderData;
import com.deepoove.poi.plugin.markdown.MarkdownRenderPolicy;
import com.deepoove.poi.plugin.markdown.MarkdownStyle;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @author xiaomifeng1010
* @version 1.0
* @date: 2024-08-24 17:23
* @Description
*/
@UtilityClass
@Slf4j
public class MarkdownUtil {
/**
* markdown转html
*
* @param markdownContent
* @return
*/
public String markdownToHtml(String markdownContent) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdownContent);
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(document);
log.info(htmlContent);
return htmlContent;
}
/**
* 将markdown格式内容转换为word并保存在本地
*
* @param markdownContent
* @param outputFileName
*/
public void toDoc(String markdownContent, String outputFileName) {
log.info("markdownContent:{}", markdownContent);
MarkdownRenderData code = new MarkdownRenderData();
code.setMarkdown(markdownContent);
MarkdownStyle style = MarkdownStyle.newStyle();
style = setMarkdownStyle(style);
code.setStyle(style);
// markdown样式处理与word模板中的标签{{md}}绑定
Map<String, Object> data = new HashMap<>();
data.put("md", code);
Configure config = Configure.builder().bind("md", new MarkdownRenderPolicy()).build();
try {
// 获取classpath
String path = MarkdownUtil.class.getClassLoader().getResource("").getPath();
log.info("classpath:{}", path);
//由于部署到linux上后,程序是从jar包中去读取resources下的文件的,所以需要使用流的方式读取,所以获取流,而不是直接使用文件路径
// 所以可以这样获取 InputStream resourceAsStream = MarkdownUtil.class.getClassLoader().getResourceAsStream("");
// 建议使用spring的工具类来获取,如下
ClassPathResource resource = new ClassPathResource("markdown" + File.separator + "markdown_template.docx");
InputStream resourceAsStream = resource.getInputStream();
XWPFTemplate.compile(resourceAsStream, config)
.render(data)
.writeToFile(path + "out_markdown_" + outputFileName + ".docx");
} catch (IOException e) {
log.error("保存为word出错");
}
}
/**
* 将markdown转换为word文档并下载
*
* @param markdownContent
* @param response
* @param fileName
*/
public void convertAndDownloadWordDocument(String markdownContent, HttpServletResponse response, String fileName) {
log.info("markdownContent:{}", markdownContent);
MarkdownRenderData code = new MarkdownRenderData();
code.setMarkdown(markdownContent);
MarkdownStyle style = MarkdownStyle.newStyle();
style = setMarkdownStyle(style);
code.setStyle(style);
// markdown样式处理与word模板中的标签{{md}}绑定
Map<String, Object> data = new HashMap<>();
data.put("md", code);
Configure configure = Configure.builder().bind("md", new MarkdownRenderPolicy()).build();
try {
fileName=URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
//由于部署到linux上后,程序是从jar包中去读取resources下的文件的,所以需要使用流的方式读取,所以获取流,而不是直接使用文件路径
// 所以可以这样获取 InputStream resourceAsStream = MarkdownUtil.class.getClassLoader().getResourceAsStream("");
// 建议使用spring的工具类来获取,如下
ClassPathResource resource = new ClassPathResource("markdown" + File.separator + "markdown_template.docx");
InputStream resourceAsStream = resource.getInputStream();
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8") + ".docx");
// contentType不设置也是也可以的,可以正常解析到
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=utf-8");
XWPFTemplate template = XWPFTemplate.compile(resourceAsStream, configure)
.render(data);
template.writeAndClose(response.getOutputStream());
} catch (IOException e) {
log.error("下载word文档失败:{}", e.getMessage());
}
}
/**
* 设置转换为word文档时的基本样式
* @param style
* @return
*/
public MarkdownStyle setMarkdownStyle(MarkdownStyle style) {
// 一定设置为false,不然生成的word文档中各元素前边都会加上有层级效果的一串数字,
// 比如一级标题 前边出现1 二级标题出现1.1 三级标题出现1.1.1这样的数字
style.setShowHeaderNumber(false);
// 修改默认的表格样式
// table header style(表格头部,通常为表格顶部第一行,用于设置列标题)
RowStyle headerStyle = new RowStyle();
CellStyle cellStyle = new CellStyle();
// 设置表格头部的背景色为灰色
cellStyle.setBackgroundColor("cccccc");
Style textStyle = new Style();
// 设置表格头部的文字颜色为黑色
textStyle.setColor("000000");
// 头部文字加粗
textStyle.setBold(true);
// 设置表格头部文字大小为12
textStyle.setFontSize(12);
// 设置表格头部文字垂直居中
cellStyle.setVertAlign(XWPFTableCell.XWPFVertAlign.CENTER);
cellStyle.setDefaultParagraphStyle(ParagraphStyle.builder().withDefaultTextStyle(textStyle).build());
headerStyle.setDefaultCellStyle(cellStyle);
style.setTableHeaderStyle(headerStyle);
// table border style(表格边框样式)
BorderStyle borderStyle = new BorderStyle();
// 设置表格边框颜色为黑色
borderStyle.setColor("000000");
// 设置表格边框宽度为3px
borderStyle.setSize(3);
// 设置表格边框样式为实线
borderStyle.setType(XWPFTable.XWPFBorderType.SINGLE);
style.setTableBorderStyle(borderStyle);
// 设置普通的引用文本样式
ParagraphStyle quoteStyle = new ParagraphStyle();
// 设置段落样式
quoteStyle.setSpacingBeforeLines(0.5d);
quoteStyle.setSpacingAfterLines(0.5d);
// 设置段落的文本样式
Style quoteTextStyle = new Style();
quoteTextStyle.setColor("000000");
quoteTextStyle.setFontSize(8);
quoteTextStyle.setItalic(true);
quoteStyle.setDefaultTextStyle(quoteTextStyle);
style.setQuoteStyle(quoteStyle);
return style;
}
public static void main(String[] args) {
String markdownContent = "# 一级标题\n" +
"## 二级标题\n" +
"### 三级标题\n" +
"#### 四级标题\n" +
"##### 五级标题\n" +
"###### 六级标题\n" +
"## 段落\n" +
"这是一段普通的段落。\n" +
"## 列表\n" +
"### 无序列表\n" +
"- 项目1\n" +
"- 项目2\n" +
"- 项目3\n" +
"### 有序列表\n" +
"1. 项目1\n" +
"2. 项目2\n" +
"3. 项目3\n" +
"## 链接\n" +
"[百度](https://www.baidu.com)\n" +
"## 图片\n" +
"\n" +
"## 表格\n" +
"| 表头1 | 表头2 | 表头3 |\n" +
"|-------|-------|-------|\n" +
"| 单元格1 | 单元格2 | 单元格3 |\n" +
"| 单元格4 | 单元格5 | 单元格6 |";
toDoc(markdownContent, "test23");
}
}
4.最终的输出效果
注意在生成有序列表或者无序列表时候不要加ref方法,因为加了ref方法虽然可以正常生成markdown文本,但是在转换成word内容的时候,commonmark会报错org.commonmark.node.OrderedList cannot be cast to org.commonmark.node.Paragra
还有一个注意事项,就是换行不要直接调用br()方法,因为转换的时候,在word文档中转换不了,直接生成了“<br>” 这样的文字,所以直接在markdown文本中使用"\n"来换行