前一篇写的是markdown格式的文本内容转换保存为word文档,是假定已经有一个现成的markdown格式的文本,然后直接转换保存为word文档,不过在开发中,通常情况下,数据是从数据库中获取,拿到的数据映射到java对象上,这一篇就是处理如何将java对象数据生成为markdown文本
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();}@Overridepublic 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;}@Overridepublic 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.google.common.collect.Lists;
import com.ruoyi.common.markdown.MarkdownHandler;
import com.ruoyi.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();
// 首页大标题String title = javaToMarkdownGenerator.generateTitle("经济环境分析\n");
// 首页目录String catalog = javaToMarkdownGenerator.generatecatalog();
// 插入项目本地的logo图片String imgPath = JavaToMarkdownGenerator.class.getResource("/static/canton.jpg").getPath();String logo = javaToMarkdownGenerator.resloveImg(imgPath, "logo");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" +"![图片描述](https://www.baidu.com/img/bd_logo1.png)\n" +"## 表格\n" +"| 表头1 | 表头2 | 表头3 |\n" +"|-------|-------|-------|\n" +"| 单元格1 | 单元格2 | 单元格3 |\n" +"| 单元格4 | 单元格5 | 单元格6 |";theWholeMarkdownContent=theWholeMarkdownContent+markdownContent;MarkdownUtil.toDoc(theWholeMarkdownContent,"test225");System.out.println(catalog);}/*** 使用markdown的有序列表实现生成目录效果* @return*/public String generatecatalog(){String md = MarkdownHandler.of().title("目录")
// 不要加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 "!["+imgName+"]("+imgPath+")";}}@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.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.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
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.setShowHeaderNumber(true);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 {
// 获取classpathString path = MarkdownUtil.class.getClassLoader().getResource("").getPath();log.info("classpath:{}", path);XWPFTemplate.compile(path + "markdown" + File.separator + "markdown_template.docx", 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.setShowHeaderNumber(true);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 {//获取classpathString path = MarkdownUtil.class.getClassLoader().getResource("").getPath();log.info("classpath:{}", path);XWPFTemplate template = XWPFTemplate.compile(path + "markdown" + File.separator + "markdown_template.docx", config).render(data);template.writeAndClose(response.getOutputStream());response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8") + ".docx");response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=utf-8");} catch (IOException e) {log.error("下载word文档失败:{}", e.getMessage());}}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" +"![图片描述](https://www.baidu.com/img/bd_logo1.png)\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"来换行