Skip to content

Commit 3c233e6

Browse files
committed
docs(plans): commit markdown-docx conversion plan (work shipped)
1 parent bbfb8a6 commit 3c233e6

1 file changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
# Markdown ↔ docx 转换实现计划
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** 给 easydoc 增加 Markdown ↔ docx 快速转换:`EasyMarkdown.markdownToDocx(...)`(MD→HTML→现有 docx 管线)与 `EasyMarkdown.docxToMarkdown(...)`(docx→HTML→简化 MD)。
6+
7+
**Architecture:** 复用现有 HTML↔docx 管线,只新增 Markdown 转换层。MD→docx 用 flexmark(`com.vladsch.flexmark:flexmark-all:0.64.8`,Java 8 兼容)将 Markdown 渲染为 HTML,再走现有 `WordprocessingMLHtmlTemplate.process(html, ...)`(docx4j-ImportXHTML);docx→MD 用现有 `WordprocessingMLPackageWriter.writeToHtml` 输出 HTML,再以简化标签映射转回 Markdown(标题/段落/表格/列表/强调)。新门面 `EasyMarkdown`(对齐 EasyDocx 风格),薄封装不替代引擎。
8+
9+
**Tech Stack:** Java 21(3.0.x;代码保持 Java 8 兼容供 1.0.x/2.0.x 同步)、flexmark-all 0.64.8、JUnit 5、docx4j 17.0.3、Maven 4 + POM 4.1.0 + `${revision}`
10+
11+
## Global Constraints
12+
13+
- 新代码放 `easydoc-xhtml`(复用 HTML 管线),包 `io.github.easy4j.doc.xhtml.markdown`
14+
- **纯新增,零破坏**:不改现有 `WordprocessingMLHtmlTemplate`/`WordprocessingMLPackageWriter` 签名;现有测试全绿
15+
- **Java 8 语法兼容**(禁 record/sealed/var/switch 表达式/instanceof pattern)——可同步 1.0.x/2.0.x
16+
- 每个 Task 末尾跑 `mvn -Denforcer.skip=true -pl easydoc-xhtml -am clean verify`(Maven 4:`~/tools/apache-maven-4.0.0-rc-6/bin/mvn`;1.0/2.0 同步时用各自 JDK/Maven)必须 BUILD SUCCESS
17+
- flexmark 依赖:`com.vladsch.flexmark:flexmark-all:0.64.8`(根 pom dependencyManagement 新增属性 `flexmark.version=0.64.8`;easydoc-xhtml 声明)
18+
- 测试用 JUnit 5,命名 `*Test.java`
19+
- 提交信息遵循现有风格(`feat(markdown): ...`
20+
21+
---
22+
23+
### Task 1: flexmark 依赖 + MarkdownConverter.mdToHtml
24+
25+
**Files:**
26+
- Modify: `pom.xml`(根,加 `flexmark.version` 属性 + dependencyManagement 条目)
27+
- Modify: `easydoc-xhtml/pom.xml`(声明 flexmark-all 依赖)
28+
- Create: `easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/MarkdownConverter.java`
29+
- Test: `easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/MarkdownConverterTest.java`
30+
31+
**Interfaces:**
32+
- Produces: `public static String mdToHtml(String markdown)` —— Markdown → HTML 字符串,供 Task 2 的 `markdownToDocx` 使用
33+
34+
- [ ] **Step 1: 写失败测试**
35+
36+
`MarkdownConverterTest.java`
37+
```java
38+
package io.github.easy4j.doc.xhtml.markdown;
39+
40+
import static org.junit.jupiter.api.Assertions.assertTrue;
41+
42+
import org.junit.jupiter.api.Test;
43+
44+
class MarkdownConverterTest {
45+
46+
@Test
47+
void mdToHtmlConvertsHeadingsAndParagraphs() {
48+
String html = MarkdownConverter.mdToHtml("# Title\n\nHello **world**.");
49+
assertTrue(html.contains("<h1>"), "heading must render to <h1>");
50+
assertTrue(html.contains("<strong>world</strong>"), "bold must render to <strong>");
51+
assertTrue(html.contains("<p>"), "paragraph must render to <p>");
52+
}
53+
54+
@Test
55+
void mdToHtmlConvertsTable() {
56+
String html = MarkdownConverter.mdToHtml("| A | B |\n|---|---|\n| 1 | 2 |");
57+
assertTrue(html.contains("<table>"), "GFM table must render to <table>");
58+
assertTrue(html.contains("<td>1</td>"), "table cell must render");
59+
}
60+
61+
@Test
62+
void mdToHtmlConvertsCodeBlockAndList() {
63+
String html = MarkdownConverter.mdToHtml("```java\nint x=1;\n```\n\n- item1\n- item2");
64+
assertTrue(html.contains("<pre>"), "code block must render to <pre>");
65+
assertTrue(html.contains("<li>item1</li>"), "list item must render");
66+
}
67+
}
68+
```
69+
70+
- [ ] **Step 2: 跑测试确认失败**
71+
72+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=MarkdownConverterTest`
73+
Expected: 编译失败(找不到 `MarkdownConverter`
74+
75+
- [ ] **Step 3: 加依赖 + 实现**
76+
77+
`pom.xml` properties 加:
78+
```xml
79+
<!-- Markdown 解析(MD→HTML 渲染,Java 8 兼容) -->
80+
<flexmark.version>0.64.8</flexmark.version>
81+
```
82+
`pom.xml` dependencyManagement 加:
83+
```xml
84+
<dependency>
85+
<groupId>com.vladsch.flexmark</groupId>
86+
<artifactId>flexmark-all</artifactId>
87+
<version>${flexmark.version}</version>
88+
</dependency>
89+
```
90+
`easydoc-xhtml/pom.xml` dependencies 加:
91+
```xml
92+
<!-- flexmark:Markdown → HTML(GFM 表格/代码块扩展) -->
93+
<dependency>
94+
<groupId>com.vladsch.flexmark</groupId>
95+
<artifactId>flexmark-all</artifactId>
96+
</dependency>
97+
```
98+
99+
`markdown/MarkdownConverter.java`
100+
```java
101+
package io.github.easy4j.doc.xhtml.markdown;
102+
103+
import com.vladsch.flexmark.ast.Node;
104+
import com.vladsch.flexmark.html.HtmlRenderer;
105+
import com.vladsch.flexmark.parser.Parser;
106+
import com.vladsch.flexmark.util.data.MutableDataSet;
107+
108+
/**
109+
* Markdown 文本 → HTML 字符串转换(flexmark 驱动,启用 GFM 表格/删除线扩展)。
110+
* 输出 HTML 供 easydoc 现有 HTML→docx 管线消费。
111+
*/
112+
public final class MarkdownConverter {
113+
114+
private static final Parser PARSER;
115+
private static final HtmlRenderer RENDERER;
116+
117+
static {
118+
MutableDataSet options = new MutableDataSet();
119+
options.set(Parser.EXTENSIONS, java.util.Arrays.asList(
120+
com.vladsch.flexmark.ext.tables.TablesExtension.create(),
121+
com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension.create(),
122+
com.vladsch.flexmark.ext.autolink.AutolinkExtension.create()));
123+
PARSER = Parser.builder(options).build();
124+
RENDERER = HtmlRenderer.builder(options).build();
125+
}
126+
127+
private MarkdownConverter() {
128+
}
129+
130+
/** Markdown → HTML。null 输入返回空串。 */
131+
public static String mdToHtml(String markdown) {
132+
if (markdown == null) {
133+
return "";
134+
}
135+
Node document = PARSER.parse(markdown);
136+
return RENDERER.render(document);
137+
}
138+
}
139+
```
140+
141+
- [ ] **Step 4: 跑测试确认通过**
142+
143+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=MarkdownConverterTest`
144+
Expected: PASS
145+
146+
- [ ] **Step 5: 提交**
147+
148+
```bash
149+
git add pom.xml easydoc-xhtml/pom.xml easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/ easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/
150+
git commit -m "feat(markdown): add flexmark dep + MarkdownConverter.mdToHtml"
151+
```
152+
153+
---
154+
155+
### Task 2: EasyMarkdown.markdownToDocx
156+
157+
**Files:**
158+
- Create: `easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdown.java`
159+
- Test: `easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdownTest.java`
160+
161+
**Interfaces:**
162+
- Consumes: `MarkdownConverter.mdToHtml`(Task 1)、现有 `WordprocessingMLHtmlTemplate`
163+
- Produces: `public static WordprocessingMLPackage markdownToDocx(String markdown)``public static WordprocessingMLPackage markdownToDocx(String markdown, Map<String,Object> vars)`
164+
165+
- [ ] **Step 1: 写失败测试**
166+
167+
`EasyMarkdownTest.java`
168+
```java
169+
package io.github.easy4j.doc.xhtml.markdown;
170+
171+
import static org.junit.jupiter.api.Assertions.assertNotNull;
172+
import static org.junit.jupiter.api.Assertions.assertTrue;
173+
174+
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
175+
import org.junit.jupiter.api.Test;
176+
177+
class EasyMarkdownTest {
178+
179+
@Test
180+
void markdownToDocxProducesPackage() throws Exception {
181+
WordprocessingMLPackage pkg = EasyMarkdown.markdownToDocx(
182+
"# 标题\n\n这是 **加粗** 内容。\n\n- 列表一\n- 列表二");
183+
assertNotNull(pkg);
184+
String xml = pkg.getMainDocumentPart().getXML();
185+
assertTrue(xml.contains("标题"), "heading text must appear in the docx");
186+
assertTrue(xml.contains("加粗"), "bold text must appear in the docx");
187+
}
188+
189+
@Test
190+
void markdownToDocxHandlesNullAndEmpty() throws Exception {
191+
assertNotNull(EasyMarkdown.markdownToDocx(null), "null markdown yields a package");
192+
assertNotNull(EasyMarkdown.markdownToDocx(""), "empty markdown yields a package");
193+
}
194+
}
195+
```
196+
197+
- [ ] **Step 2: 跑测试确认失败**
198+
199+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=EasyMarkdownTest`
200+
Expected: 编译失败(找不到 `EasyMarkdown`
201+
202+
- [ ] **Step 3: 实现门面**
203+
204+
`markdown/EasyMarkdown.java`
205+
```java
206+
package io.github.easy4j.doc.xhtml.markdown;
207+
208+
import java.util.Map;
209+
210+
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
211+
212+
import io.github.easy4j.doc.xhtml.WordprocessingMLHtmlTemplate;
213+
214+
/**
215+
* Markdown ↔ docx 转换门面(对齐 EasyDocx 风格)。MD→docx 走
216+
* MarkdownConverter.mdToHtml + 现有 WordprocessingMLHtmlTemplate(HTML→docx);
217+
* 薄封装,不替代引擎。
218+
*/
219+
public final class EasyMarkdown {
220+
221+
private EasyMarkdown() {
222+
}
223+
224+
/** Markdown → docx(无变量替换)。 */
225+
public static WordprocessingMLPackage markdownToDocx(String markdown) throws Exception {
226+
return markdownToDocx(markdown, null);
227+
}
228+
229+
/** Markdown → docx(支持 ${var} 占位符替换——MD 内容渲染后由 HTML 管线处理)。 */
230+
public static WordprocessingMLPackage markdownToDocx(String markdown,
231+
Map<String, Object> vars) throws Exception {
232+
String html = MarkdownConverter.mdToHtml(markdown);
233+
WordprocessingMLHtmlTemplate template = new WordprocessingMLHtmlTemplate();
234+
return template.process(html, vars);
235+
}
236+
}
237+
```
238+
239+
- [ ] **Step 4: 跑测试确认通过**
240+
241+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=EasyMarkdownTest`
242+
Expected: PASS(若 docx4j-ImportXHTML 对 flexmark 输出(如 `<table>`/`<pre>`)解析有兼容问题,调整 flexmark 扩展或 HTML 预处理——报告具体失败)
243+
244+
- [ ] **Step 5: 提交**
245+
246+
```bash
247+
git add easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdown.java easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdownTest.java
248+
git commit -m "feat(markdown): add EasyMarkdown.markdownToDocx"
249+
```
250+
251+
---
252+
253+
### Task 3: EasyMarkdown.docxToMarkdown
254+
255+
**Files:**
256+
- Modify: `easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdown.java`
257+
- Modify: `easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/MarkdownConverter.java`(加 `docxToMarkdown` 辅助)
258+
- Test: `easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/EasyMarkdownTest.java`(追加)
259+
260+
**Interfaces:**
261+
- Consumes: `WordprocessingMLPackageWriter.writeToHtml`(docx→HTML)、`MarkdownConverter.docxToMarkdown(String html)`(HTML→MD)
262+
- Produces: `public static String docxToMarkdown(WordprocessingMLPackage pkg)`
263+
264+
- [ ] **Step 1: 追加失败测试**
265+
266+
`EasyMarkdownTest.java` 追加:
267+
```java
268+
@Test
269+
void docxToMarkdownConvertsHeadingAndBold() throws Exception {
270+
WordprocessingMLPackage pkg = EasyMarkdown.markdownToDocx("# 标题\n\n**加粗** 内容");
271+
String md = EasyMarkdown.docxToMarkdown(pkg);
272+
assertTrue(md.contains("标题"), "heading text must appear in markdown output");
273+
assertTrue(md.contains("加粗"), "text must appear in markdown output");
274+
}
275+
```
276+
277+
- [ ] **Step 2: 跑测试确认失败**
278+
279+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=EasyMarkdownTest`
280+
Expected: FAIL(`docxToMarkdown` 不存在)
281+
282+
- [ ] **Step 3: 实现 docx→Markdown(docx→HTML→简化 MD)**
283+
284+
`MarkdownConverter.java` 追加(HTML→MD 简化映射):
285+
```java
286+
/** HTML → Markdown(简化映射:标题/段落/粗斜体/列表/表格/代码块)。 */
287+
public static String htmlToMarkdown(String html) {
288+
if (html == null) {
289+
return "";
290+
}
291+
String out = html
292+
.replaceAll("(?i)<h1[^>]*>", "\n# ")
293+
.replaceAll("(?i)</h1>", "\n")
294+
.replaceAll("(?i)<h2[^>]*>", "\n## ")
295+
.replaceAll("(?i)</h2>", "\n")
296+
.replaceAll("(?i)<h3[^>]*>", "\n### ")
297+
.replaceAll("(?i)</h3>", "\n")
298+
.replaceAll("(?i)<strong>", "**").replaceAll("(?i)</strong>", "**")
299+
.replaceAll("(?i)<em>", "*").replaceAll("(?i)</em>", "*")
300+
.replaceAll("(?i)<li>", "- ").replaceAll("(?i)</li>", "\n")
301+
.replaceAll("(?i)<p[^>]*>", "\n").replaceAll("(?i)</p>", "\n")
302+
.replaceAll("(?i)<td[^>]*>", " | ").replaceAll("(?i)</td>", "")
303+
.replaceAll("(?i)</tr>", "\n")
304+
.replaceAll("(?i)<pre[^>]*>", "\n```\n").replaceAll("(?i)</pre>", "\n```\n")
305+
.replaceAll("(?i)</?table[^>]*>", "\n")
306+
.replaceAll("(?i)</?thead[^>]*>|</?tbody[^>]*>|</?tr[^>]*>", "\n")
307+
.replaceAll("(?i)<[^>]+>", ""); // 残余标签
308+
return out.replaceAll("\\n{3,}", "\n\n").trim();
309+
}
310+
```
311+
312+
`EasyMarkdown.java` 追加:
313+
```java
314+
/** docx → Markdown(经 docx4j HTML 导出 + 简化 HTML→MD 映射)。 */
315+
public static String docxToMarkdown(org.docx4j.openpackaging.packages.WordprocessingMLPackage pkg)
316+
throws Exception {
317+
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
318+
new io.github.easy4j.doc.io.WordprocessingMLPackageWriter().writeToHtml(pkg, out);
319+
return MarkdownConverter.htmlToMarkdown(out.toString("UTF-8"));
320+
}
321+
```
322+
323+
- [ ] **Step 4: 跑测试确认通过**
324+
325+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn -Denforcer.skip=true -pl easydoc-xhtml -am test -Dtest=EasyMarkdownTest`
326+
Expected: PASS(docx4j HTML 导出的标签与映射匹配;若 `writeToHtml(OutputStream)` 签名/行为差异,按 easydoc-xhtml 实际 API 适配)
327+
328+
- [ ] **Step 5: 提交**
329+
330+
```bash
331+
git add easydoc-xhtml/src/main/java/io/github/easy4j/doc/xhtml/markdown/ easydoc-xhtml/src/test/java/io/github/easy4j/doc/xhtml/markdown/
332+
git commit -m "feat(markdown): add EasyMarkdown.docxToMarkdown"
333+
```
334+
335+
---
336+
337+
### Task 4: 全量验证 + README/CHANGELOG + 推送
338+
339+
**Files:**
340+
- Modify: `README.md`(Markdown 转换快速上手段落)
341+
- Modify: `CHANGELOG.md`
342+
343+
- [ ] **Step 1: README 增加 Markdown 转换**
344+
345+
在 README.md 的 EasyDocx 段后追加:
346+
```markdown
347+
### Markdown ↔ docx
348+
349+
```java
350+
// Markdown → docx(快速转换,复用 HTML 管线)
351+
org.docx4j.openpackaging.packages.WordprocessingMLPackage doc =
352+
EasyMarkdown.markdownToDocx("# 标题\n\n**加粗** 内容");
353+
354+
// docx → Markdown
355+
String md = EasyMarkdown.docxToMarkdown(doc);
356+
```
357+
```
358+
359+
- [ ] **Step 2: 全量验证**
360+
361+
Run: `~/tools/apache-maven-4.0.0-rc-6/bin/mvn clean verify`(Maven 4 无 skip)
362+
Expected: BUILD SUCCESS(13 模块 + JaCoCo 90%)
363+
364+
- [ ] **Step 3: CHANGELOG 记录**
365+
366+
`CHANGELOG.md` 3.0.x 段追加:
367+
```markdown
368+
- **Markdown ↔ docx**`EasyMarkdown.markdownToDocx`(MD→HTML→docx,flexmark 驱动)、
369+
`EasyMarkdown.docxToMarkdown`(docx→HTML→MD 简化映射)
370+
```
371+
372+
- [ ] **Step 4: 提交 + 推送**
373+
374+
```bash
375+
git add README.md CHANGELOG.md
376+
git commit -m "docs(markdown): document Markdown conversion + changelog"
377+
git push origin feature/3.0.x
378+
git checkout main && git merge --ff-only feature/3.0.x && git push origin main
379+
```
380+
381+
---
382+
383+
## 执行后同步(1.0.x / 2.0.x)
384+
385+
全部代码 Java 8 兼容 + flexmark 0.64.8 是 Java 8 库;依赖 API(`WordprocessingMLHtmlTemplate.process(html, vars)``WordprocessingMLPackageWriter.writeToHtml(pkg, OutputStream)`)在 1.0/2.0 存在(同步时核对签名:1.0/2.0 的 `writeToHtml` 是否接受 `OutputStream`——若无,用 `File` 重载 + 读回字符串适配)。
386+
387+
同步步骤(对应 worktree):
388+
1. 根 pom 加 `flexmark.version=0.64.8` + dependencyManagement;`easydoc-xhtml/pom.xml` 加 flexmark-all
389+
2. 复制 `xhtml/.../markdown/` 源码与测试到 1.0.x/2.0.x
390+
3. 适配 `docxToMarkdown``writeToHtml` 调用(核对 1.0/2.0 签名)
391+
4. 各分支 `mvn -Denforcer.skip=true -pl easydoc-xhtml -am clean verify` 绿
392+
5. commit + push feature/1.0.x / feature/2.0.x

0 commit comments

Comments
 (0)