Skip to content

Commit 2a20f92

Browse files
committed
feat(markdown): sync Markdown<->docx from 3.0.x (JDK 17 adaptation)
- flexmark 0.64.8 (Java 11+, OK on JDK 17) - MarkdownConverter + EasyMarkdown copied from 3.0.x; docx4j 11.5.14 HTMLSettings/Docx4J.toHTML compatible; test-time physical font discovery disabled (docx4j 11.5.14 IdentityPlusMapper assertion on JDK 17) - Tests: MarkdownConverterTest 4 + EasyMarkdownTest 3 Full verify (JDK 17): BUILD SUCCESS, xhtml 39 tests.
1 parent 2040dcf commit 2a20f92

7 files changed

Lines changed: 211 additions & 0 deletions

File tree

easydoc-xhtml/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
<artifactId>docx4j-ImportXHTML-core</artifactId>
4848
</dependency>
4949

50+
<!-- flexmark:Markdown → HTML(GFM 表格/代码块扩展) -->
51+
<dependency>
52+
<groupId>com.vladsch.flexmark</groupId>
53+
<artifactId>flexmark-all</artifactId>
54+
</dependency>
55+
5056
<!-- ======== Test ======== -->
5157

5258
<!-- eclipselink:JPA 实现,测试环境用于 docx4j MOXy JAXB 兼容验证 -->
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.github.easy4j.doc.xhtml.markdown;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.util.Map;
5+
6+
import org.docx4j.Docx4J;
7+
import org.docx4j.convert.out.HTMLSettings;
8+
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
9+
10+
import io.github.easy4j.doc.xhtml.WordprocessingMLHtmlTemplate;
11+
12+
/**
13+
* Markdown ↔ docx 转换门面(对齐 EasyDocx 风格)。MD→docx 走
14+
* MarkdownConverter.mdToHtml + 现有 WordprocessingMLHtmlTemplate(HTML→docx);
15+
* 薄封装,不替代引擎。
16+
*/
17+
public final class EasyMarkdown {
18+
19+
private EasyMarkdown() {
20+
}
21+
22+
/** Markdown → docx(无变量替换)。 */
23+
public static WordprocessingMLPackage markdownToDocx(String markdown) throws Exception {
24+
return markdownToDocx(markdown, null);
25+
}
26+
27+
/** Markdown → docx。variables 参数当前未生效(WordprocessingMLHtmlTemplate 暂不支持 HTML 变量替换);传入 null 即可。 */
28+
public static WordprocessingMLPackage markdownToDocx(String markdown,
29+
Map<String, Object> vars) throws Exception {
30+
String html = MarkdownConverter.mdToHtml(markdown);
31+
WordprocessingMLHtmlTemplate template = new WordprocessingMLHtmlTemplate();
32+
return template.process(html, vars);
33+
}
34+
35+
/** docx → Markdown(经 docx4j HTML 导出 + 简化 HTML→MD 映射)。null 输入返回空串。 */
36+
public static String docxToMarkdown(WordprocessingMLPackage pkg) throws Exception {
37+
if (pkg == null) {
38+
return "";
39+
}
40+
// 无现成的 writeToHtml(pkg, OutputStream) 封装可用,
41+
// 故直接使用 docx4j API(HTMLSettings + Docx4J.toHTML)导出到内存流。
42+
HTMLSettings htmlSettings = Docx4J.createHTMLSettings();
43+
htmlSettings.setWmlPackage(pkg);
44+
// 防止含图片文档在 HTML 导出时因未设置 imageDirPath 而失败
45+
htmlSettings.setImageDirPath(System.getProperty("java.io.tmpdir"));
46+
htmlSettings.setImageTargetUri("images");
47+
ByteArrayOutputStream out = new ByteArrayOutputStream();
48+
Docx4J.toHTML(htmlSettings, out, Docx4J.FLAG_EXPORT_PREFER_XSL);
49+
return MarkdownConverter.htmlToMarkdown(out.toString("UTF-8"));
50+
}
51+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.github.easy4j.doc.xhtml.markdown;
2+
3+
import com.vladsch.flexmark.html.HtmlRenderer;
4+
import com.vladsch.flexmark.parser.Parser;
5+
import com.vladsch.flexmark.util.ast.Node;
6+
import com.vladsch.flexmark.util.data.MutableDataSet;
7+
8+
/**
9+
* Markdown 文本 → HTML 字符串转换(flexmark 驱动,启用 GFM 表格/删除线扩展)。
10+
* 输出 HTML 供 easydoc 现有 HTML→docx 管线消费。
11+
*/
12+
public final class MarkdownConverter {
13+
14+
private static final Parser PARSER;
15+
private static final HtmlRenderer RENDERER;
16+
17+
static {
18+
MutableDataSet options = new MutableDataSet();
19+
options.set(Parser.EXTENSIONS, java.util.Arrays.asList(
20+
com.vladsch.flexmark.ext.tables.TablesExtension.create(),
21+
com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension.create(),
22+
com.vladsch.flexmark.ext.autolink.AutolinkExtension.create()));
23+
PARSER = Parser.builder(options).build();
24+
RENDERER = HtmlRenderer.builder(options).build();
25+
}
26+
27+
private MarkdownConverter() {
28+
}
29+
30+
/** Markdown → HTML。null 输入返回空串。 */
31+
public static String mdToHtml(String markdown) {
32+
if (markdown == null) {
33+
return "";
34+
}
35+
Node document = PARSER.parse(markdown);
36+
return RENDERER.render(document);
37+
}
38+
39+
/** HTML → Markdown(简化映射:标题/段落/粗斜体/列表/表格/代码块)。 */
40+
public static String htmlToMarkdown(String html) {
41+
if (html == null) {
42+
return "";
43+
}
44+
String out = html
45+
.replaceAll("(?i)<h1[^>]*>", "\n# ")
46+
.replaceAll("(?i)</h1>", "\n")
47+
.replaceAll("(?i)<h2[^>]*>", "\n## ")
48+
.replaceAll("(?i)</h2>", "\n")
49+
.replaceAll("(?i)<h3[^>]*>", "\n### ")
50+
.replaceAll("(?i)</h3>", "\n")
51+
.replaceAll("(?i)<strong[^>]*>", "**").replaceAll("(?i)</strong>", "**")
52+
.replaceAll("(?i)<em[^>]*>", "*").replaceAll("(?i)</em>", "*")
53+
.replaceAll("(?i)<li[^>]*>", "- ").replaceAll("(?i)</li>", "\n")
54+
.replaceAll("(?i)<p[^>]*>", "\n").replaceAll("(?i)</p>", "\n")
55+
.replaceAll("(?i)<td[^>]*>", " | ").replaceAll("(?i)</td>", "")
56+
.replaceAll("(?i)</tr>", "\n")
57+
.replaceAll("(?i)<pre[^>]*>", "\n```\n").replaceAll("(?i)</pre>", "\n```\n")
58+
.replaceAll("(?i)</?table[^>]*>", "\n")
59+
.replaceAll("(?i)</?thead[^>]*>|</?tbody[^>]*>|</?tr[^>]*>", "\n")
60+
.replaceAll("(?i)<[^>]+>", ""); // 残余标签
61+
return out.replaceAll("\\n{3,}", "\n\n").trim();
62+
}
63+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.github.easy4j.doc.xhtml.markdown;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
7+
import org.junit.jupiter.api.Test;
8+
9+
class EasyMarkdownTest {
10+
11+
@Test
12+
void markdownToDocxProducesPackage() throws Exception {
13+
WordprocessingMLPackage pkg = EasyMarkdown.markdownToDocx(
14+
"# 标题\n\n这是 **加粗** 内容。\n\n- 列表一\n- 列表二");
15+
assertNotNull(pkg);
16+
String xml = pkg.getMainDocumentPart().getXML();
17+
assertTrue(xml.contains("标题"), "heading text must appear in the docx");
18+
assertTrue(xml.contains("加粗"), "bold text must appear in the docx");
19+
}
20+
21+
@Test
22+
void markdownToDocxHandlesNullAndEmpty() throws Exception {
23+
assertNotNull(EasyMarkdown.markdownToDocx(null), "null markdown yields a package");
24+
assertNotNull(EasyMarkdown.markdownToDocx(""), "empty markdown yields a package");
25+
}
26+
27+
@Test
28+
void docxToMarkdownConvertsHeadingAndBold() throws Exception {
29+
WordprocessingMLPackage pkg = EasyMarkdown.markdownToDocx("# 标题\n\n**加粗** 内容");
30+
String md = EasyMarkdown.docxToMarkdown(pkg);
31+
assertTrue(md.contains("标题"), "heading text must appear in markdown output");
32+
assertTrue(md.contains("加粗"), "text must appear in markdown output");
33+
}
34+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.github.easy4j.doc.xhtml.markdown;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class MarkdownConverterTest {
8+
9+
@Test
10+
void mdToHtmlConvertsHeadingsAndParagraphs() {
11+
String html = MarkdownConverter.mdToHtml("# Title\n\nHello **world**.");
12+
assertTrue(html.contains("<h1>"), "heading must render to <h1>");
13+
assertTrue(html.contains("<strong>world</strong>"), "bold must render to <strong>");
14+
assertTrue(html.contains("<p>"), "paragraph must render to <p>");
15+
}
16+
17+
@Test
18+
void mdToHtmlConvertsTable() {
19+
String html = MarkdownConverter.mdToHtml("| A | B |\n|---|---|\n| 1 | 2 |");
20+
assertTrue(html.contains("<table>"), "GFM table must render to <table>");
21+
assertTrue(html.contains("<td>1</td>"), "table cell must render");
22+
}
23+
24+
@Test
25+
void mdToHtmlConvertsCodeBlockAndList() {
26+
String html = MarkdownConverter.mdToHtml("```java\nint x=1;\n```\n\n- item1\n- item2");
27+
assertTrue(html.contains("<pre>"), "code block must render to <pre>");
28+
assertTrue(html.contains("<li>item1</li>"), "list item must render");
29+
}
30+
31+
@Test
32+
void htmlToMarkdownHandlesTagsWithAttributes() {
33+
String md = MarkdownConverter.htmlToMarkdown(
34+
"<strong class=\"x\">bold</strong> and <li class=\"y\">item</li>");
35+
assertTrue(md.contains("**bold**"), "strong with attribute must become **bold**");
36+
assertTrue(md.contains("- item"), "li with attribute must become list item");
37+
}
38+
}

easydoc-xhtml/src/test/resources/docx4j.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ docx4j.javax.xml.parsers.DocumentBuilderFactory.donotset = false
3737
#docx4j.fonts.fop.util.FopConfigUtil.substitutions =
3838
#docx4j.fonts.microsoft.MicrosoftFonts = org/docx4j/fonts/microsoft/MicrosoftFonts.xml
3939

40+
# 禁用物理字体自动发现:docx4j 11.5.14 解析部分 macOS 系统字体的 GPOS 表时,
41+
# GlyphPositioningTable$DeviceTable 的 assert 会在启用断言的测试 JVM 中抛出
42+
# AssertionError(EasyMarkdown.docxToMarkdown 的 Docx4J.toHTML 路径触发);
43+
# HTML 导出与 MD 转换不依赖物理字体发现,生产环境(默认不开 -ea)不受影响。
44+
docx4j.fonts.discoverPhysicalFonts.enabled = false
45+
4046

4147
docx4j.model.datastorage.placeholder = OpenDoPE/placeholder.xml
4248
docx4j.model.datastorage.OpenDoPEReverter.Supported = true

pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@
7979
<commons-io.version>2.22.0</commons-io.version>
8080
<commons-lang3.version>3.20.0</commons-lang3.version>
8181
<commons-logging.version>1.3.5</commons-logging.version>
82+
<!-- Markdown 解析(MD→HTML 渲染,Java 8 兼容) -->
83+
<flexmark.version>0.64.8</flexmark.version>
8284
<jackson-bom.version>2.22.1</jackson-bom.version>
8385
<javax.mail-api.version>1.6.2</javax.mail-api.version>
8486
<jsoup.version>1.16.1</jsoup.version>
@@ -746,6 +748,17 @@
746748
<artifactId>jsoup</artifactId>
747749
<version>${jsoup.version}</version>
748750
</dependency>
751+
752+
<!-- ======== Markdown 解析 ======== -->
753+
754+
<!-- https://mvnrepository.com/artifact/com.vladsch.flexmark/flexmark-all -->
755+
<!-- flexmark 聚合构件(Markdown → HTML,含 GFM 表格/删除线/自动链接扩展,easydoc-xhtml 使用) -->
756+
<dependency>
757+
<groupId>com.vladsch.flexmark</groupId>
758+
<artifactId>flexmark-all</artifactId>
759+
<version>${flexmark.version}</version>
760+
</dependency>
761+
749762
<!-- ======== Docx4j 全家桶(11.5.14) ======== -->
750763

751764
<!-- https://mvnrepository.com/artifact/org.docx4j/docx4j-core -->

0 commit comments

Comments
 (0)