2. AI 输出一键导出 Word!docx4j+poi-tl 实现 Markdown 转 Word 全流程(含自界说样式 + 占位符更换)

[复制链接]
发表于 2025-11-26 17:40:34 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1.简介

我们在上一章先容了如果想实现将markdown内容转换为word的话, 如果想要转换后的word内容排版悦目的话, 就必要将其转换过程分为两步

  • markdown→html
  • html→ooxml(Office Open XML) word内容,word元信息本身就是个xml)
上一章节我们使用flexmark将markdown内容转换为html内容, 完成了第一步, 本章节我们将先容怎样将html转换为ooxml
2. 情况信息

为了兼容更多的场景, 以是并没有用一些高版本的SDK, 信息如下
  1. Java: 8
  2. Docx4j: 8.3.10
复制代码
3. Maven
  1. <properties>
  2.   <docx4j.version>8.3.10</docx4j.version>
  3.   <jaxb2.version>1.11.1</jaxb2.version>
  4. </properties>
  5. <dependencies>
  6.   <dependency>
  7.     <groupId>org.docx4j</groupId>
  8.     <artifactId>docx4j-JAXB-Internal</artifactId>
  9.     <version>${docx4j.version}</version>
  10.   </dependency>
  11.   <dependency>
  12.     <groupId>org.docx4j</groupId>
  13.     <artifactId>docx4j-ImportXHTML</artifactId>
  14.     <version>${docx4j.version}</version>
  15.   </dependency>
  16.   <dependency>
  17.     <groupId>org.docx4j</groupId>
  18.     <artifactId>docx4j-JAXB-MOXy</artifactId>
  19.     <version>${docx4j.version}</version>
  20.   </dependency>
  21.   <dependency>
  22.     <groupId>org.jvnet.jaxb2_commons</groupId>
  23.     <artifactId>jaxb2-basics</artifactId>
  24.     <version>${jaxb2.version}</version>
  25.   </dependency>
  26. </dependencies>
复制代码
4. Html转Docx
  1. import lombok.SneakyThrows;
  2. import org.docx4j.Docx4J;
  3. import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
  4. import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
  5. import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
  6. import org.docx4j.wml.Body;
  7. import java.io.File;
  8. /**
  9. * html 2 docx
  10. *
  11. * @author ludangxin
  12. * @since 2025/10/14
  13. */
  14. public class HtmlToDocx {
  15.     @SneakyThrows
  16.     public static void convertHtmlToDocx(String htmlContent, String outputFilePath) {
  17.         // 创建 Word 文档包
  18.         WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage();
  19.         MainDocumentPart mainDocumentPart = wordMLPackage.getMainDocumentPart();
  20.         // 设置 XHTML 导入器
  21.         XHTMLImporterImpl XHTMLImporter = new XHTMLImporterImpl(wordMLPackage);
  22.         // 将 HTML 内容导入到 Word 文档中
  23.         Body body = mainDocumentPart.getJaxbElement().getBody();
  24.         body.getContent().addAll(XHTMLImporter.convert(htmlContent, null));
  25.         // 保存 Word 文档
  26.         Docx4J.save(wordMLPackage, new File(outputFilePath), Docx4J.FLAG_NONE);
  27.     }
  28.     public static void main(String[] args) {
  29.         String html = "<html><head></head><body><h2>嘉文四世</h2>\n" + "<blockquote>\n" + "<p>德玛西亚</p>\n" + "</blockquote>\n" + "<p><strong>给我找些更强的敌人!</strong></p>\n" + "<table>\n" + "<thead>\n" + "<tr><th>列1</th><th>列2</th></tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr><td>数据1</td><td>数据2</td></tr>\n" + "</tbody>\n" + "</table>\n" + "</body></html>";
  30.         convertHtmlToDocx(html, "demo.docx");
  31.     }
  32. }
复制代码
测试效果如下:

在根目次天生了docx文件, 文件内容如下

天生的文档内容是有样式的, 只不外像字体样式, 包罗表格都是默认样式
但如果项目上要求输出的制式文档大概是模板文件, 对笔墨标题以致是表格都有其样式要求的话, 那就得用一些高阶用法了
5. 自界说样式

如果想要自界说输出的内容样式, 着实就两个思绪:

  • 从输出的内容出发: 毕竟是html转的ooxml, 那么可以给html添加css样式给docx4j举行渲染, 但条件是一些简单的css样式
  • 从word出发: word文件本身就有内置样式而且也可以自界说样式, 以是可以先在模板文件中界说好样式, 然后和输入的内容举行映射
5.1 Html添加Css

比如给表格添加样式, 让表格有边框而且有肯定的样式,css样式如下:
  1. table{border-collapse:collapse;border-spacing:0;width:100%;margin:1em 0;background-color:transparent;}table th{background-color:#f7f7f7;border:1px solid #ddd;padding:8px 12px;text-align:left}table td{border:1px solid #ddd;padding:8px 12px}
复制代码
  1. public static void main(String[] args) {
  2.     String html = "<html><head></head><body><h2>嘉文四世</h2>\n" + "<blockquote>\n" + "<p>德玛西亚</p>\n" + "</blockquote>\n" + "<p><strong>给我找些更强的敌人!</strong></p>\n" + "<table>\n" + "<thead>\n" + "<tr><th>列1</th><th>列2</th></tr>\n" + "</thead>\n" + "<tbody>\n" + "<tr><td>数据1</td><td>数据2</td></tr>\n" + "</tbody>\n" + "</table>\n" + "</body></html>";
  3.     convertHtmlToDocx(html, "demo.docx");
  4. }
复制代码
测试效果如下:

此时着实如果想要输出的内容样式悦目, 通过界说css根本可以满意了, 但如果是制式文档对行间距,字间距,字体型号,标题,等有严格的要求, 如果这些都通过css界说的话 有点贫苦, 毕竟人家制式的文档本身已经界说好了, 那么就可以使用下面的方式
5.2 Html映射WordStyleId

我们可以先看一下word的内置样式, 我这里使用的是mac office,windows 和 wps 有些许差异

从上图中可以看到, word着实是有很多内置样式的, 而且可以新建样式, 下面也可以筛选列表
我们经常在快捷样式列表中选择的样式着实就是从这里来的

我们先手动新增一个自界说的样式 如下:

然后通过docx4j获取全部的wordstyle列表 如下:
  1. private static WordprocessingMLPackage wordMLPackage;
  2. @BeforeAll
  3. @SneakyThrows
  4. public static void init_mainDocumentPart() {
  5.           File templateFile = new File("demo.docx");
  6.           wordMLPackage = WordprocessingMLPackage.load(templateFile);
  7. }
  8. @Test
  9. @SneakyThrows
  10. public void given_doc_template_when_extract_style_then_return_style_list() {
  11.           final StyleDefinitionsPart sdp = wordMLPackage.getMainDocumentPart().getStyleDefinitionsPart();
  12.           List</head><body><h2 class="1">嘉文四世</h2><blockquote><p class="customBodyText">德玛西亚</p></blockquote><p class="customBodyText"><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>";
  13.           final MainDocumentPart mainDocumentPart = wordMLPackage.getMainDocumentPart();
  14.           XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage);
  15.           // CLASS_TO_STYLE_ONLY:只认 class,不管 style 和 <strong>/<em> 等标签,相当于「纯 CSS 类驱动样式」
  16.           // CLASS_PLUS_OTHER:class 是基础样式,style 和内联标签是补充 / 覆盖,相当于「类样式 + 局部微调样式」
  17.           // IGNORE_CLASS: 忽略class样式
  18.           importer.setParagraphFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  19.           importer.setRunFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  20.           importer.setTableFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  21.           // html转ooxml
  22.           final List<Object> docxContent = importer.convert(html, null);
  23.           final List<Object> docxOldContent = mainDocumentPart.getContent();
  24.           // 清空模板内容 并 添加新的内容
  25.           docxOldContent.clear();
  26.           docxOldContent.addAll(docxContent);
  27.           Docx4J.save(wordMLPackage, new File("newDemo.docx"), Docx4J.FLAG_NONE);
  28. }
复制代码
测试效果如下:
一级标题和自界说样式都映射完成了

6. 占位符更换

联合上述的功能, 已经能很好的输出html到word中了, 但项目上又有新的需求了, 不光是将markdown→html→word, 还必要将大模子辨认到的内容一同输出到模板文件中去, 也就是终极输出的内容有两部门

  • 大模子提取到的职员信息, 如姓名工作住址等
  • 大模子总结的职员形貌信息(markdown)
着实第二步内容使用doc4j已经实现了, 如今必要通过占位符的方式输出职员根本信息到word中, 这个着实很长处置惩罚, 可以使用poi-tl实现占位符的更换
6.1 Maven
  1. <dependency>
  2.           <groupId>com.deepoove</groupId>
  3.           <artifactId>poi-tl</artifactId>
  4.           <version>1.12.0</version>
  5. </dependency>
复制代码
6.2 实现
  1. @Test
  2. public void given_template_doc_and_content_when_replace_then_replace() {
  3.     final Configure templateEngineConfigure = Configure.builder().build();
  4.     File templateFile = new File("demo.docx");
  5.     File outputFile = new File("newDemo.docx");
  6.     Map<String, Object> data = new HashMap<>();
  7.     data.put("user", "嘉文四世");
  8.         data.put("summoner", "张铁牛");
  9.     data.put("position", "打野");
  10.     data.put("dialogue", "给我找些更强的敌人");
  11.     try (XWPFTemplate template = XWPFTemplate.compile(templateFile, templateEngineConfigure)) {
  12.             template.render(data).writeToFile(outputFile.getAbsolutePath());
  13.         }
  14.         catch (IOException e) {
  15.             log.error("failed to replace template word placeholder", e);
  16.             throw new RuntimeException(e);
  17.         }
  18. }
复制代码
模板内容如下:

测试效果如下:
不但实现了占位符的更换, 而且也生存了占位符本身的样式, 这就很舒服了

7. 封装工具类

为了更方便的调用docx4j和poi-tl操纵word, 我们可以封装一个工具类去更方便的调用, 比如可以通过传入一个map对象然后实现主动更换占位符和markdown内容渲染, 最好是通过链式调用一行代码就办理斗斗, 没错 它来了
  1. import com.deepoove.poi.XWPFTemplate;
  2. import com.deepoove.poi.config.Configure;
  3. import lombok.SneakyThrows;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.docx4j.convert.in.xhtml.FormattingOption;
  6. import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
  7. import org.docx4j.openpackaging.exceptions.Docx4JException;
  8. import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
  9. import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
  10. import org.docx4j.wml.Body;
  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.io.OutputStream;
  15. import java.nio.file.Files;
  16. import java.util.ArrayList;
  17. import java.util.List;
  18. import java.util.Map;
  19. import java.util.Optional;
  20. import java.util.function.BiFunction;
  21. /**
  22. * doc操作工具类
  23. *
  24. * @author ludangxin
  25. * @since 2025/10/14
  26. */
  27. @Slf4j
  28. public class Docs {
  29.     @SneakyThrows
  30.     public static DocBuilder builder() {
  31.         return new DocBuilder().wordMLPackage(WordprocessingMLPackage.createPackage());
  32.     }
  33.     @SneakyThrows
  34.     public static DocBuilder builder(File file) {
  35.         return new DocBuilder().templateInputStream(Files.newInputStream(file.toPath()))
  36.                                .wordMLPackage(WordprocessingMLPackage.load(file));
  37.     }
  38.     @SneakyThrows
  39.     public static DocBuilder builder(InputStream inputStream) {
  40.         return new DocBuilder().templateInputStream(inputStream)
  41.                                .wordMLPackage(WordprocessingMLPackage.load(inputStream));
  42.     }
  43.     @SneakyThrows
  44.     public static DocBuilder builder(String filePath) {
  45.         return new DocBuilder().templateInputStream(Files.newInputStream(new File(filePath).toPath()))
  46.                                .wordMLPackage(WordprocessingMLPackage.load(new File(filePath)));
  47.     }
  48.     public static class DocBuilder {
  49.         private InputStream templateInputStream;
  50.         private WordprocessingMLPackage wordMLPackage;
  51.         private XHTMLImporterImpl importer;
  52.         private FormattingOption paragraphFormatting;
  53.         private FormattingOption runFormatting;
  54.         private FormattingOption tableFormatting;
  55.         private String staticResourceBaseUri;
  56.         private String[] placeHolderPreSuffix = new String[]{"{{", "}}"};
  57.         private Configure templateEngineConfigure;
  58.         private boolean useHtmlDefaultStyle = true;
  59.         private boolean autoCloseStream = true;
  60.         private String globalCss = "table{border-collapse:collapse;border-spacing:0;width:100%;margin:1em 0;background-color:transparent;}table th{background-color:#f7f7f7;border:1px solid #ddd;padding:8px 12px;text-align:left}table td{border:1px solid #ddd;padding:8px 12px}";
  61.         /**
  62.          * <String, String, String>: htmlContent htmlKey resultHtmlContent
  63.          */
  64.         private BiFunction<String, String, String> htmlContentProcessor;
  65.         private DocBuilder templateInputStream(InputStream templateInputStream) {
  66.             this.templateInputStream = templateInputStream;
  67.             return this;
  68.         }
  69.         private DocBuilder wordMLPackage(WordprocessingMLPackage wordMLPackage) {
  70.             this.wordMLPackage = wordMLPackage;
  71.             return this;
  72.         }
  73.         public DocBuilder importer(XHTMLImporterImpl importer) {
  74.             this.importer = importer;
  75.             return this;
  76.         }
  77.         public DocBuilder paragraphFormatting(FormattingOption paragraphFormatting) {
  78.             this.paragraphFormatting = paragraphFormatting;
  79.             return this;
  80.         }
  81.         public DocBuilder runFormatting(FormattingOption runFormatting) {
  82.             this.runFormatting = runFormatting;
  83.             return this;
  84.         }
  85.         public DocBuilder tableFormatting(FormattingOption tableFormatting) {
  86.             this.tableFormatting = tableFormatting;
  87.             return this;
  88.         }
  89.         public DocBuilder useHtmlDefaultStyle(boolean useHtmlDefaultStyle) {
  90.             this.useHtmlDefaultStyle = useHtmlDefaultStyle;
  91.             return this;
  92.         }
  93.         public DocBuilder staticResourceBaseUri(String staticResourceBaseUri) {
  94.             this.staticResourceBaseUri = staticResourceBaseUri;
  95.             return this;
  96.         }
  97.         public DocBuilder placeHolderPreSuffix(String placeHolderPrefix, String placeHolderSuffix) {
  98.             this.placeHolderPreSuffix = new String[]{placeHolderPrefix, placeHolderSuffix};
  99.             return this;
  100.         }
  101.         public DocBuilder templateEngineConfigure(Configure templateEngineConfigure) {
  102.             this.templateEngineConfigure = templateEngineConfigure;
  103.             return this;
  104.         }
  105.         public DocBuilder autoCloseStream(boolean autoCloseStream) {
  106.             this.autoCloseStream = autoCloseStream;
  107.             return this;
  108.         }
  109.         public DocBuilder globalCss(String globalCss) {
  110.             this.globalCss = globalCss;
  111.             return this;
  112.         }
  113.         public DocBuilder htmlContentProcessor(BiFunction<String, String, String> htmlContentProcessor) {
  114.             this.htmlContentProcessor = htmlContentProcessor;
  115.             return this;
  116.         }
  117.         public List<Object> buildWordML(String html) {
  118.             return this.buildWordML(html, null);
  119.         }
  120.         public void buildWord(String html, String outputFile) {
  121.             this.buildWord(html, new File(outputFile));
  122.         }
  123.         public void buildWord(String html, File outputFile) {
  124.             try {
  125.                 this.getMainContent()
  126.                     .addAll(this.buildWordML(html));
  127.                 wordMLPackage.save(outputFile);
  128.             }
  129.             catch (Exception e) {
  130.                 log.error("failed to build word file", e);
  131.                 throw new RuntimeException(e);
  132.             }
  133.         }
  134.         public void buildWord(String html, OutputStream outputStream) {
  135.             try {
  136.                 this.getMainContent()
  137.                     .addAll(this.buildWordML(html));
  138.                 wordMLPackage.save(outputStream);
  139.             }
  140.             catch (Exception e) {
  141.                 log.error("failed to build word file", e);
  142.                 throw new RuntimeException(e);
  143.             }
  144.             finally {
  145.                 try {
  146.                     if (autoCloseStream) {
  147.                         outputStream.close();
  148.                     }
  149.                 }
  150.                 catch (IOException ignored) {
  151.                 }
  152.             }
  153.         }
  154.         public void buildWord(Map<String, Object> placeHolderData, OutputStream outputStream) {
  155.             try {
  156.                 // 替换模板中的普通占位符
  157.                 if (this.checkPlaceHolderDataType(placeHolderData) == 1) {
  158.                     this.replacePlaceHolder(placeHolderData, outputStream);
  159.                 }
  160.                 // 替换模板中包含的html
  161.                 if (this.checkPlaceHolderDataType(placeHolderData) == 2) {
  162.                     this.replaceHtmlPlaceHolder(placeHolderData, outputStream);
  163.                 }
  164.                 // 替换普通/html占位符
  165.                 if (this.checkPlaceHolderDataType(placeHolderData) == 3) {
  166.                     final File tempDocFile = DocUtils.createTempDocFile();
  167.                     this.replaceHtmlPlaceHolder(placeHolderData, tempDocFile);
  168.                     this.replacePlaceHolder(placeHolderData, tempDocFile, tempDocFile);
  169.                     DocUtils.writeAndDeleteFile(tempDocFile, outputStream);
  170.                 }
  171.             }
  172.             catch (Exception e) {
  173.                 log.error("failed to build word file", e);
  174.                 throw new RuntimeException(e);
  175.             }
  176.             finally {
  177.                 try {
  178.                     if (autoCloseStream) {
  179.                         outputStream.close();
  180.                     }
  181.                 }
  182.                 catch (IOException ignored) {
  183.                 }
  184.             }
  185.         }
  186.         public void buildWord(Map<String, Object> placeHolderData, File outputFile) {
  187.             // 替换模板中的普通占位符
  188.             if (this.checkPlaceHolderDataType(placeHolderData) == 1) {
  189.                 this.replacePlaceHolder(placeHolderData, outputFile);
  190.             }
  191.             // 替换模板中包含的html
  192.             if (this.checkPlaceHolderDataType(placeHolderData) > 1) {
  193.                 this.replaceHtmlPlaceHolder(placeHolderData, outputFile);
  194.             }
  195.             // 追加替换普通占位符
  196.             if (this.checkPlaceHolderDataType(placeHolderData) == 3) {
  197.                 this.replacePlaceHolder(placeHolderData, outputFile, outputFile);
  198.             }
  199.         }
  200.         private List<Object> buildWordML(String html, String htmlKey) {
  201.             final XHTMLImporterImpl importer = this.getImporterOrDefault();
  202.             try {
  203.                 if (globalCss != null && !globalCss.isEmpty()) {
  204.                     html = DocUtils.addHtmlStyles(html, globalCss);
  205.                 }
  206.                 if (htmlContentProcessor != null) {
  207.                     html = htmlContentProcessor.apply(html, htmlKey);
  208.                 }
  209.                 return importer.convert(html, staticResourceBaseUri);
  210.             }
  211.             catch (Exception e) {
  212.                 log.error("failed to convert HTML to XHTML", e);
  213.                 throw new RuntimeException(e);
  214.             }
  215.         }
  216.         private void replaceHtmlPlaceHolder(Map<String, Object> placeHolderData, File outputFile) {
  217.             this.doReplaceHtmlPlaceHolder(placeHolderData);
  218.             try {
  219.                 // 替换html
  220.                 wordMLPackage.save(outputFile);
  221.             }
  222.             catch (Docx4JException e) {
  223.                 log.error("failed to build word file", e);
  224.                 throw new RuntimeException(e);
  225.             }
  226.         }
  227.         private void replaceHtmlPlaceHolder(Map<String, Object> placeHolderData, OutputStream outputStream) {
  228.             this.doReplaceHtmlPlaceHolder(placeHolderData);
  229.             try {
  230.                 // 替换html
  231.                 wordMLPackage.save(outputStream);
  232.             }
  233.             catch (Docx4JException e) {
  234.                 log.error("failed to build word file", e);
  235.                 throw new RuntimeException(e);
  236.             }
  237.             finally {
  238.                 try {
  239.                     if (autoCloseStream) {
  240.                         outputStream.close();
  241.                     }
  242.                 }
  243.                 catch (IOException ignored) {
  244.                 }
  245.             }
  246.         }
  247.         private void doReplaceHtmlPlaceHolder(Map<String, Object> placeHolderData) {
  248.             final List<Object> mainContent = this.getMainContent();
  249.             List<Object> newContent = new ArrayList<>();
  250.             for (Object p : mainContent) {
  251.                 String text = DocUtils.extractText(p);
  252.                 Optional<String> matchedKey = placeHolderData.keySet()
  253.                                                              .stream()
  254.                                                              .filter(key -> DocUtils.matchPlaceHolder(text, key, placeHolderPreSuffix[0], placeHolderPreSuffix[1]))
  255.                                                              .findFirst();
  256.                 if (matchedKey.isPresent()) {
  257.                     String key = matchedKey.get();
  258.                     Object value = placeHolderData.get(key);
  259.                     if (DocUtils.isHtml(value)) {
  260.                         final List<Object> wordFragment = this.buildWordML((String) value, key);
  261.                         newContent.addAll(wordFragment);
  262.                     }
  263.                     else {
  264.                         newContent.add(p);
  265.                     }
  266.                 }
  267.                 else {
  268.                     newContent.add(p);
  269.                 }
  270.             }
  271.             // 替换模板内容
  272.             mainContent.clear();
  273.             mainContent.addAll(newContent);
  274.         }
  275.         private void replacePlaceHolder(Map<String, Object> data, File templateFile, File outputFile) {
  276.             final Configure templateEngineConfigure = this.getTemplateEngineConfigureOrDefault();
  277.             try (XWPFTemplate template = XWPFTemplate.compile(templateFile, templateEngineConfigure)){
  278.                 template.render(data)
  279.                         .writeToFile(outputFile.getAbsolutePath());
  280.             }
  281.             catch (IOException e) {
  282.                 log.error("failed to replace template word placeholder", e);
  283.                 throw new RuntimeException(e);
  284.             }
  285.         }
  286.         public void replacePlaceHolder(Map<String, Object> data, File outputFile) {
  287.             final Configure templateEngineConfigure = this.getTemplateEngineConfigureOrDefault();
  288.             if (templateInputStream == null) {
  289.                 throw new NullPointerException("template file can not be null");
  290.             }
  291.             XWPFTemplate template = XWPFTemplate.compile(templateInputStream, templateEngineConfigure);
  292.             try {
  293.                 template.render(data)
  294.                         .writeToFile(outputFile.getAbsolutePath());
  295.             }
  296.             catch (IOException e) {
  297.                 log.error("failed to replace template word placeholder", e);
  298.                 throw new RuntimeException(e);
  299.             }
  300.         }
  301.         public void replacePlaceHolder(Map<String, Object> data, String outputFileAbsolutePath) {
  302.             final Configure templateEngineConfigure = this.getTemplateEngineConfigureOrDefault();
  303.             if (templateInputStream == null) {
  304.                 throw new NullPointerException("template file can not be null");
  305.             }
  306.             XWPFTemplate template = XWPFTemplate.compile(templateInputStream, templateEngineConfigure);
  307.             try {
  308.                 template.render(data)
  309.                         .writeToFile(outputFileAbsolutePath);
  310.             }
  311.             catch (IOException e) {
  312.                 log.error("failed to replace template word placeholder", e);
  313.                 throw new RuntimeException(e);
  314.             }
  315.         }
  316.         public void replacePlaceHolder(Map<String, Object> data, OutputStream outputStream) {
  317.             final Configure templateEngineConfigure = this.getTemplateEngineConfigureOrDefault();
  318.             if (templateInputStream == null) {
  319.                 throw new NullPointerException("template file can not be null");
  320.             }
  321.             try {
  322.                 XWPFTemplate template = XWPFTemplate.compile(templateInputStream, templateEngineConfigure);
  323.                 final XWPFTemplate render = template.render(data);
  324.                 render.write(outputStream);
  325.             }
  326.             catch (IOException e) {
  327.                 log.error("failed to replace template word placeholder", e);
  328.                 throw new RuntimeException(e);
  329.             }
  330.             finally {
  331.                 try {
  332.                     if (autoCloseStream) {
  333.                         outputStream.close();
  334.                     }
  335.                 }
  336.                 catch (IOException ignored) {
  337.                 }
  338.             }
  339.         }
  340.         private XHTMLImporterImpl getImporterOrDefault() {
  341.             if (importer == null) {
  342.                 if (paragraphFormatting != null || runFormatting != null || tableFormatting != null) {
  343.                     XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage);
  344.                     importer.setParagraphFormatting(paragraphFormatting == null ? FormattingOption.CLASS_PLUS_OTHER : paragraphFormatting);
  345.                     importer.setRunFormatting(runFormatting == null ? FormattingOption.CLASS_PLUS_OTHER : runFormatting);
  346.                     importer.setTableFormatting(tableFormatting == null ? FormattingOption.CLASS_PLUS_OTHER : tableFormatting);
  347.                     return importer;
  348.                 }
  349.                 else {
  350.                     return this.defaultImporter();
  351.                 }
  352.             }
  353.             else {
  354.                 return this.importer;
  355.             }
  356.         }
  357.         private Configure getTemplateEngineConfigureOrDefault() {
  358.             if (templateEngineConfigure == null) {
  359.                 return this.defaultTemplateEngineConfigure();
  360.             }
  361.             else {
  362.                 return this.templateEngineConfigure;
  363.             }
  364.         }
  365.         private Configure defaultTemplateEngineConfigure() {
  366.             return Configure.builder()
  367.                             .buildGramer(placeHolderPreSuffix[0], placeHolderPreSuffix[1])
  368.                             .build();
  369.         }
  370.         private XHTMLImporterImpl defaultImporter() {
  371.             XHTMLImporterImpl importer = new XHTMLImporterImpl(wordMLPackage);
  372.             if (useHtmlDefaultStyle) {
  373.                 importer.setParagraphFormatting(FormattingOption.CLASS_PLUS_OTHER);
  374.                 importer.setRunFormatting(FormattingOption.CLASS_PLUS_OTHER);
  375.                 importer.setTableFormatting(FormattingOption.CLASS_PLUS_OTHER);
  376.             }
  377.             else {
  378.                 importer.setParagraphFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  379.                 importer.setRunFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  380.                 importer.setTableFormatting(FormattingOption.CLASS_TO_STYLE_ONLY);
  381.             }
  382.             return importer;
  383.         }
  384.         private List<Object> getMainContent() {
  385.             MainDocumentPart mainDocumentPart = wordMLPackage.getMainDocumentPart();
  386.             if (globalCss != null && !globalCss.isEmpty()) {
  387.                 mainDocumentPart.getStyleDefinitionsPart()
  388.                                 .setCss(globalCss);
  389.             }
  390.             Body body = mainDocumentPart.getJaxbElement()
  391.                                         .getBody();
  392.             return body.getContent();
  393.         }
  394.         /**
  395.          * 判断占位符数据类型
  396.          *
  397.          * @param placeHolderData 占位符数据
  398.          * @return 1: 数据不包含html 2: 数据全是html 3: 都包含
  399.          */
  400.         private int checkPlaceHolderDataType(Map<String, Object> placeHolderData) {
  401.             boolean hasHtmlValFlag = false;
  402.             boolean hasCommonValFlag = false;
  403.             for (Object value : placeHolderData.values()) {
  404.                 if (DocUtils.isHtml(value)) {
  405.                     hasHtmlValFlag = true;
  406.                 }
  407.                 else {
  408.                     hasCommonValFlag = true;
  409.                 }
  410.             }
  411.             if (!hasHtmlValFlag && hasCommonValFlag) {
  412.                 return 1;
  413.             }
  414.             if (hasHtmlValFlag && !hasCommonValFlag) {
  415.                 return 2;
  416.             }
  417.             return 3;
  418.         }
  419.     }
  420. }
复制代码
8. 测试示例
  1. import lombok.SneakyThrows;
  2. import org.junit.jupiter.api.BeforeAll;
  3. import org.junit.jupiter.api.Test;
  4. import java.io.File;
  5. import java.io.OutputStream;
  6. import java.nio.file.Files;
  7. import java.util.HashMap;
  8. import java.util.Map;
  9. /**
  10. * docs test
  11. *
  12. * @author ludangxin
  13. * @since 2025/11/4
  14. */
  15. public class DocxTest {
  16.     private static final File TEMPLATE_FILE = new File("demo.docx");
  17.     private static final File OUTPUT_FILE = new File("output.docx");
  18.     private static final Map<String, Object> DATA = new HashMap<>();
  19.     @BeforeAll
  20.     public static void given_data() {
  21.         DATA.put("user", "嘉文四世");
  22.         DATA.put("summoner", "张铁牛");
  23.         DATA.put("position", "打野");
  24.         DATA.put("dialogue", "给我找些更强的敌人");
  25.         final String markdownContent = "- **背景故事**:嘉文四世是德玛西亚国王嘉文三世的独生子,其母凯瑟琳女士因难产而死。嘉文在宫廷中长大,接受了良好的德玛西亚式教育,并结识了赵信,向其学习战争艺术。他与盖伦年龄相仿,结为好兄弟。嘉文曾率军前往边境对抗诺克萨斯,却因战力分散而战败,幸得希瓦娜相救。后来,德玛西亚国内搜魔人兵团搜捕魔法师引发起义,嘉文三世惨遭弑杀,嘉文四世接掌了议会,之后他登基成为德玛西亚国王。\n" + "\n" + "- **角色定位**:在游戏中,嘉文四世的定位是坦克、战士,他常常需要带头冲入敌方阵地,因此相比输出更加需要增强防御能力。\n" + "\n" + "- 技能介绍\n" + "  :\n" + "  - **被动技能 - 战争律动**:普攻命中时,会对目标造成 8% 当前生命值的额外物理伤害,该效果作用于同一目标的冷却时间为 6 秒。\n" + "  - **一技能 - 巨龙撞击**:用长矛穿透路径上的敌人,对其造成物理伤害,并减少其护甲,持续 3 秒。若长矛触及 “德邦军旗”,嘉文四世会被引向军旗,并击飞沿途敌人 0.75 秒。\n" + "  - **二技能 - 黄金圣盾**:释放出一道帝王光环,使周围敌人减速,持续 2 秒,同时提供一个可以吸收伤害的护盾,持续 5 秒,附近每多一名敌方英雄,吸收伤害增加。\n" + "  - **三技能 - 德邦军旗**:投掷一柄军旗,对敌人造成魔法伤害,并将军旗置于原地 8 秒,使附近队友获得攻击速度加成。在 “德邦军旗” 附近再次点击施放该技能,将会朝军旗施放 “巨龙撞击”。\n" + "  - **终极技能 - 天崩地裂**:跃向敌方英雄,对目标及其附近的敌人造成物理伤害,并在目标周围形成环形障碍,持续 3.5 秒,再次点击施放可使障碍倒塌。\n" + "\n" + "- **皮肤信息**:嘉文四世拥有多款皮肤,包括孤胆英豪、暗星、福牛守护者等。";
  26.         // markdown 2 html (上一章博客的内容)
  27.         final String htmlContent = Markdowns.builder(markdownContent)
  28.                                             .buildHtmlContent();
  29.         DATA.put("description", htmlContent);
  30.     }
  31.     @Test
  32.     public void given_template_doc_and_content_when_replace_then_complete() {
  33.         Docs.builder(TEMPLATE_FILE).buildWord(DATA, OUTPUT_FILE);
  34.     }
  35.     @Test
  36.     @SneakyThrows
  37.     public void given_template_doc_and_content_when_replace_and_output_stream_then_complete() {
  38.         final OutputStream fileOutputStream = Files.newOutputStream(OUTPUT_FILE.toPath());
  39.         // 接收输出流
  40.         Docs.builder(TEMPLATE_FILE).autoCloseStream(true).buildWord(DATA, fileOutputStream);
  41.     }
  42. }
复制代码
模板内容如下:

测试效果如下:

9. 小结

本章使用docx4j和poi-tl实现将平凡占位符内容和html文本内容转换为word, 并先容了怎样使用其特性实现自界说样式渲染, 末了封装链式调用的工具类和对应的单元测试代码, 联合上一章内容可以或许将各种情势的内容通过一行代码即可实现word的渲染
10. 源码

测试过程中的代码已全部上传至github, 接待点赞收藏 堆栈所在: https://github.com/ludangxin/markdown2docx
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表