当前位置: 首页 > news >正文

基于Java的Markdown转Word工具(标题、段落、表格、Echarts图等)

项目源于我们开发的一款基于大模型的报告生成工具。由于需要将 Markdown 格式的内容导出为 Word 文档,而市面上缺乏合适的现成工具,所以决定自己开发一个Markdown转Word的工具。

🩷源码地址:daydayup-zyn/md2doc-plus

😀实现思路

md2doc-plus 基于 Java 17 和 Apache POI 构建,采用模块化设计,主要包括以下几个核心组件:

  1. Markdown 解析器:负责解析 Markdown 内容,识别文本、表格、图表等元素
  2. 文档生成器:使用 Apache POI 创建和操作 Word 文档
  3. 模板引擎:支持动态生成文档模板,便于后续内容替换
  4. 图表转换器:专门处理 ECharts 图表到 Word 图表的转换

😃核心转换流程如下:

  • 解析 Markdown 内容,识别各种元素(标题、段落、表格、图表等)
  • 基于 Markdown 结构,自动化创建 Word 文档模板,为动态内容预留占位符
  • 将解析后的内容填充到模板中
  • 生成最终的 Word 文档

😁功能亮点

  1. 完整的 Markdown 支持

    md2doc-plus 支持常见的 Markdown 语法元素:

    • 各级标题(H1-H6)
    • 段落文本
    • 表格
    • ECharts 图表
  2. ECharts 图表支持
    这是 md2doc-plus 的一大亮点。它能够解析 Markdown 中的 ECharts 配置代码块,并将其转换为 Word 中的图表对象:

    ‍```echarts
    {title: {text: '月度销售数据'},tooltip: {trigger: 'axis'},xAxis: {type: 'category',data: ['1月', '2月', '3月', '4月', '5月', '6月']},yAxis: {type: 'value',name: '销售额'},series: [{name: '销售额',type: 'line',data: [15.32, 15.87, 14.96, 16.23, 13.21, 13.53]}]
    }
    ‍```
    
  3. 动态模板生成
    工具支持动态生成 Word 模板,能够根据 Markdown 内容自动创建包含占位符的文档结构,便于后续内容填充。

  4. 高度可定制
    通过 WordParams 和 ChartTable 等模型类,用户可以灵活地自定义生成的 Word 文档内容和格式。

  5. 易于集成
    作为基于 Java 的工具库,md2doc-plus 可以轻松集成到现有的 Java 项目中,为应用程序提供 Markdown 到 Word 的转换能力。

🫠核心实现代码解析

  1. Markdown 解析与文档结构创建
    md2doc-plus 的核心功能之一是解析 Markdown 内容并创建相应的 Word 文档结构。这主要在 DynamicWordDocumentCreator 类中实现:

    /*** 解析Markdown内容并创建Word文档结构* @param document Word文档对象* @param markdownContent Markdown内容*/
    private static void parseAndCreateDocumentStructure(XWPFDocument document, String markdownContent) {// 用于匹配ECharts代码块的正则表达式Pattern echartsPattern = Pattern.compile("‍```echarts\\s*\\n(.*?)\\n‍```", Pattern.DOTALL);// 用于匹配表格的正则表达式Pattern tablePattern = Pattern.compile("(\\|[^\\n]*\\|\\s*\\n\\s*\\|[-|\\s]*\\|\\s*\\n(?:\\s*\\|[^\\n]*\\|\\s*\\n?)*)", Pattern.MULTILINE);// 用于匹配标题的正则表达式Pattern headerPattern = Pattern.compile("^(#{1,6})\\s+(.*)$", Pattern.MULTILINE);String[] lines = markdownContent.split("\n");int chartIndex = 1;int tableIndex = 1;for (int i = 0; i < lines.length; i++) {String line = lines[i];// 检查是否为标题Matcher headerMatcher = headerPattern.matcher(line);if (headerMatcher.find()) {int level = headerMatcher.group(1).length();String title = headerMatcher.group(2);XWPFParagraph headerParagraph = document.createParagraph();setHeaderStyle(headerParagraph, level);setHeaderParagraphStyle(headerParagraph, level);XWPFRun headerRun = headerParagraph.createRun();headerRun.setText(title);headerRun.setBold(true);headerRun.setFontFamily("宋体");// 根据标题级别设置字体大小int fontSize = 16; // 默认H3switch (level) {case 1: fontSize = 22; break; // H1case 2: fontSize = 20; break; // H2case 3: fontSize = 18; break; // H3case 4: fontSize = 16; break; // H4case 5: fontSize = 14; break; // H5case 6: fontSize = 12; break; // H6}headerRun.setFontSize(fontSize);continue;}// 检查是否为ECharts图表if (line.trim().equals("‍```echarts")) {// 查找图表代码块的结束位置StringBuilder chartCode = new StringBuilder();i++; // 移动到下一行while (i < lines.length && !lines[i].trim().equals("‍```")) {chartCode.append(lines[i]).append("\n");i++;}// 创建图表占位符XWPFParagraph chartTitleParagraph = document.createParagraph();setDefaultParagraphStyle(chartTitleParagraph); // 图表标题使用默认段落样式XWPFRun chartTitleRun = chartTitleParagraph.createRun();chartTitleRun.setText("图表 " + chartIndex + ":");chartTitleRun.setBold(true);chartTitleRun.setFontFamily("宋体");// 创建实际的图表对象try {createChartInDocument(document, "chart" + chartIndex, chartCode.toString());} catch (Exception e) {// 如果创建图表失败,至少添加占位符XWPFParagraph chartParagraph = document.createParagraph();chartParagraph.setAlignment(ParagraphAlignment.CENTER);setDefaultParagraphStyle(chartParagraph);XWPFRun chartRun = chartParagraph.createRun();chartRun.setText("${chart" + chartIndex + "}");}chartIndex++;continue;}// 检查是否为表格开始if (line.startsWith("|")) {// 收集表格的所有行StringBuilder tableMarkdown = new StringBuilder(line).append("\n");i++; // 移动到下一行while (i < lines.length && (lines[i].startsWith("|") || lines[i].trim().matches("^\\|?\\s*[-|:\\s]+\\|?\\s*$"))) {tableMarkdown.append(lines[i]).append("\n");i++;}i--; // 回退一行,因为循环会自动增加i// 创建表格占位符XWPFParagraph tableTitleParagraph = document.createParagraph();setDefaultParagraphStyle(tableTitleParagraph); // 表格标题使用默认段落样式XWPFRun tableTitleRun = tableTitleParagraph.createRun();tableTitleRun.setText("表格 " + tableIndex + ":");tableTitleRun.setBold(true);tableTitleRun.setFontFamily("宋体");XWPFParagraph tableParagraph = document.createParagraph();setDefaultParagraphStyle(tableParagraph);XWPFRun tableRun = tableParagraph.createRun();tableRun.setText("${table" + tableIndex + "}");tableIndex++;continue;}// 普通段落if (!line.trim().isEmpty()) {XWPFParagraph paragraph = document.createParagraph();setDefaultParagraphStyle(paragraph); // 内容段落使用默认样式XWPFRun run = paragraph.createRun();run.setText(line);run.setFontFamily("宋体");run.setFontSize(12); // 小四号字体}}
    }
    
  2. ECharts 图表转换
    EChartsToWordConverter 类负责将 ECharts 配置转换为 Word 图表数据:

    public static void convertEChartsToWordChart(WordParams params, String chartKey, String echartsConfig) throws IOException {try {// 预处理ECharts配置,将其转换为有效的JSON格式String jsonConfig = convertEChartsToJson(echartsConfig);JsonNode rootNode = objectMapper.readTree(jsonConfig);// 获取图表标题String title = rootNode.path("title").path("text").asText("默认标题");// 创建图表ChartTable chartTable = params.addChart(chartKey).setTitle(title);// 处理 X 轴数据JsonNode xAxisNode = rootNode.path("xAxis");if (xAxisNode.isArray()) {xAxisNode = xAxisNode.get(0); // 多个 x 轴时取第一个}if (!xAxisNode.isMissingNode()) {JsonNode xAxisData = xAxisNode.path("data");if (!xAxisData.isMissingNode()) {List<String> xAxisLabels = new ArrayList<>();for (JsonNode dataNode : xAxisData) {xAxisLabels.add(dataNode.asText());}chartTable.getXAxis().addAllData(xAxisLabels);}}// 处理 Y 轴数据和系列数据JsonNode seriesNode = rootNode.path("series");if (seriesNode.isArray()) {for (JsonNode serie : seriesNode) {String seriesName = serie.path("name").asText("数据系列");JsonNode seriesData = serie.path("data");if (!seriesData.isMissingNode() && seriesData.isArray()) {List<Number> dataValues = new ArrayList<>();for (JsonNode dataNode : seriesData) {if (dataNode.isNumber()) {dataValues.add(dataNode.numberValue());} else {dataValues.add(0);}}chartTable.newYAxis(seriesName).addAllData(dataValues);}}}// 如果有 Y 轴名称设置,更新第一个 Y 轴的标题JsonNode yAxisNode = rootNode.path("yAxis");if (yAxisNode.isArray()) {yAxisNode = yAxisNode.get(0); // 多个 y 轴时取第一个}if (!yAxisNode.isMissingNode()) {String yAxisName = yAxisNode.path("name").asText("");if (!yAxisName.isEmpty() && !chartTable.getYAxis().isEmpty()) {// 获取第一个 Y 轴并设置标题String firstKey = chartTable.getYAxis().keySet().iterator().next();chartTable.getYAxis(firstKey).setTitle(yAxisName);}}} catch (Exception e) {// 如果解析失败,创建一个默认的空图表ChartTable chartTable = params.addChart(chartKey).setTitle("默认图表标题");chartTable.getXAxis().addAllData("数据1", "数据2", "数据3");chartTable.newYAxis("默认系列").addAllData(10, 20, 30);throw new IOException("解析ECharts配置时出错: " + e.getMessage(), e);}
    }
    
  3. 表格解析
    MarkdownTableParser 类负责解析 Markdown 表格:

    public static List<List<String>> parseTable(String markdownTable) {List<List<String>> tableData = new ArrayList<>();String[] lines = markdownTable.split("\n");for (String line : lines) {line = line.trim();// 跳过分隔行(只包含|和-的行)if (line.matches("^\\|?\\s*[-|:\\s]+\\|?\\s*$") && line.contains("-")) {continue;}if (line.startsWith("|")) {line = line.substring(1);}if (line.endsWith("|")) {line = line.substring(0, line.length() - 1);}String[] cells = line.split("\\|");List<String> row = new ArrayList<>();for (String cell : cells) {row.add(cell.trim());}// 只有当行不为空时才添加到表格数据中if (!row.isEmpty() && !(row.size() == 1 && row.get(0).isEmpty())) {tableData.add(row);}}return tableData;
    }
    

🥰使用示例

使用 md2doc-plus 非常简单,只需要几行代码:

public class Test {public static void main(String[] args) throws Exception {MarkdownToWordConverter.convertMarkdownFileToWord("./markdown/未命名.md","./word/未命名_output.docx");}
}

😜效果验证

  • 原始markdown文件:

    在这里插入图片描述

  • 转换后的Word文档

    在这里插入图片描述

🤨存在的问题

  1. word章节标题样式缺失,无法自动生成目录;
  2. 图表样式缺失,图表显示不全,需手动调整;
http://www.lryc.cn/news/619744.html

相关文章:

  • 18.10 SQuAD数据集实战:5步高效获取与预处理,BERT微调避坑指南
  • 实战多屏Wallpaper壁纸显示及出现黑屏问题bug分析-学员作业
  • HTML <iframe> 标签 如何把html写入iframe标签
  • 版图设计学习2_掌握PDK中的层定义(工艺文档精读)
  • Spring Boot 集成 机器人指令中枢ROS2工业机械臂控制网关
  • 如何在 Spring Boot 中设计和返回树形结构的组织和部门信息
  • 大致计算服务器磁盘使用情况脚本
  • GNhao/GN号,海外SIM号怎么获取的步骤指南
  • npm install 的作用
  • Android实现Glide/Coil样式图/视频加载框架,Kotlin
  • 【KO】Android 网络相关面试题
  • 华为 HCIE 大数据认证中 Linux 命令行的运用及价值
  • 安装Win10怎样跳过欢迎界面
  • 数字货币的去中心化:重构价值交换的底层逻辑​
  • uniapp微信小程序-登录页面验证码的实现(springboot+vue前后端分离)EasyCaptcha验证码 超详细
  • Lombok插件介绍及安装(Eclipse)
  • Python3解释器深度解析与实战教程:从源码到性能优化的全路径探索
  • Day51--图论--99. 岛屿数量(卡码网),100. 岛屿的最大面积(卡码网)
  • 【数据结构】——栈(Stack)的原理与实现
  • 最新Coze(扣子)智能体工作流:用Coze实现「图片生成-视频制作」全自动化,3分钟批量产出爆款内容
  • 自由学习记录(83)
  • 【Unity开发】Unity核心学习(一)
  • 简单了解:CS5803芯片技术解析:HDMI到V-by-One的信号转换
  • BGP特性笔记
  • Cursor替代品:亚马逊出品,Kiro免费使用Claude Sonnet4.0一款更注重流程感的 AI IDE
  • PG靶机 - PayDay
  • lowbit函数
  • 打靶日常-文件上传
  • 《Power Voronoi图的数学原理》
  • latex 中将新的一个section重新从1开始排序,而不是和前面的section继续排序