@v1hz/md2docx 1.0.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -58
- package/config/config.json +1 -1
- package/config/config.schema.json +1 -1
- package/package.json +2 -2
- package/src/cli.ts +4 -0
- package/src/index.ts +29 -3
- package/src/paths.ts +3 -6
- package/src/style/extract.ts +1 -8
- package/src/style/generate.ts +31 -0
package/README.md
CHANGED
|
@@ -1,62 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.svg" alt="md2docx" width="160" height="160">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">md2docx</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@v1hz/md2docx"><img src="https://img.shields.io/npm/v/@v1hz/md2docx" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/WXY-V1hZ/md2docx"><img src="https://img.shields.io/badge/license-MIT-blue" alt="license"></a>
|
|
14
|
+
</p>
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
- **标题层级归一化** — 自动修正标题层级跳跃,确保从 H1 开始连续递增
|
|
11
|
-
- **标题编号** — 为标题添加多级编号(1、1.1、1.1.1 …),支持剥离已有编号
|
|
12
|
-
- **表格编号** — 自动检测表格并添加"表 1"、"表 2"……编号
|
|
13
|
-
- **图片编号** — 为独立图片添加"图 1"、"图 2"……编号
|
|
14
|
-
- **Mermaid 图表** — 将 Mermaid 代码块渲染为 PNG 图片嵌入文档
|
|
15
|
-
- **Word 导出** — 集成 pandoc,一键生成 .docx 文件
|
|
16
|
+
---
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
### 前置依赖
|
|
18
|
+
## 功能
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
- [pandoc](https://pandoc.org)(可选,需导出 Word 时安装)
|
|
20
|
+
将 Markdown 转换为格式精美的 Word 文档,自动处理:
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
| 功能 | 说明 |
|
|
23
|
+
| ------------------ | ---------------------------------------------------------- |
|
|
24
|
+
| **文档标题** | 从 frontmatter、H1 或文件名自动提取 |
|
|
25
|
+
| **标题层级归一化** | 修正标题层级跳跃,确保从 H1 开始连续递增 |
|
|
26
|
+
| **标题编号** | 为标题添加多级编号(1、1.1、1.1.1 …),支持剥离已有编号 |
|
|
27
|
+
| **表格编号** | 自动为表格添加编号(表 1、表 2 …) |
|
|
28
|
+
| **图片编号** | 为独立图片添加编号(图 1、图 2 …) |
|
|
29
|
+
| **Mermaid 图表** | 将 Mermaid 代码块渲染为 PNG 图片 |
|
|
30
|
+
| **Word 导出** | 集成 pandoc,传入自定义样式模板,一键生成 .docx |
|
|
31
|
+
| **样式定制** | 通过 `style/style.json` 定义文档样式,支持从模板 docx 提取 |
|
|
32
|
+
| **web 配置编辑器** | 浏览器中可视化编辑配置 |
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
npx @v1hz/md2docx README.md
|
|
28
|
-
```
|
|
34
|
+
## 安装
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
_前置依赖_:[Bun](https://bun.sh) | [Pandoc](https://pandoc.org)
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
39
|
npm install -g @v1hz/md2docx
|
|
34
|
-
md2docx
|
|
40
|
+
md2docx docs/example.md
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
git clone https://github.com/WXY-V1hZ/md2docx.git
|
|
41
|
-
cd md2docx
|
|
42
|
-
bun install
|
|
43
|
-
bun run src/index.ts docs/example.md
|
|
44
|
-
```
|
|
43
|
+
也可通过 `npx @v1hz/md2docx` 直接运行。
|
|
45
44
|
|
|
46
45
|
## 快速开始
|
|
47
46
|
|
|
48
47
|
```bash
|
|
49
48
|
# 处理 Markdown 并生成 Word 文档
|
|
50
|
-
|
|
49
|
+
md2docx docs/example.md
|
|
51
50
|
|
|
52
51
|
# 只预处理,不调用 pandoc
|
|
53
|
-
|
|
52
|
+
md2docx docs/example.md --pandoc.enabled false
|
|
54
53
|
|
|
55
54
|
# 指定输出路径
|
|
56
|
-
|
|
55
|
+
md2docx docs/example.md -o output/example.docx
|
|
56
|
+
|
|
57
|
+
# 打开 web 网页自定义配置
|
|
58
|
+
md2docx --web
|
|
57
59
|
|
|
58
60
|
# 使用自定义配置文件
|
|
59
|
-
|
|
61
|
+
md2docx docs/example.md --config ./my-config.json
|
|
62
|
+
|
|
63
|
+
# 清除缓存(预处理中间产物、样式模板等)
|
|
64
|
+
md2docx clean
|
|
60
65
|
```
|
|
61
66
|
|
|
62
67
|
## 配置
|
|
@@ -66,62 +71,104 @@ npx @v1hz/md2docx docs/example.md --config ./my-config.json
|
|
|
66
71
|
所有配置项均可通过命令行覆盖:
|
|
67
72
|
|
|
68
73
|
```bash
|
|
69
|
-
|
|
74
|
+
md2docx docs/example.md --figureCaption.enabled false
|
|
70
75
|
```
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `numberHeadings.
|
|
80
|
-
| `figureCaption.enabled`
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
| 配置项 | 说明 | 默认值 |
|
|
78
|
+
| -------------------------------- | ------------------------------------------------------------ | --------------------- |
|
|
79
|
+
| `detectTitle.enabled` | 自动设置文档标题 | `true` |
|
|
80
|
+
| `detectTitle.strategy` | 标题来源策略:`first-h1` / `single-h1` / `filename` / `none` | `"first-h1"` |
|
|
81
|
+
| `normalizeHeadings.enabled` | 自动修正标题层级 | `true` |
|
|
82
|
+
| `numberHeadings.enabled` | 自动为标题编号(开启时自动启用 normalizeHeadings) | `true` |
|
|
83
|
+
| `numberHeadings.detectExisting` | 重新编号前移除已有编号 | `true` |
|
|
84
|
+
| `numberHeadings.useBuiltinRules` | 识别常见的中文和数字编号 | `true` |
|
|
85
|
+
| `figureCaption.enabled` | 自动为图片编号(格式:`图 {n}`) | `true` |
|
|
86
|
+
| `figureCaption.format` | 图片编号格式 | `"图 {n}"` |
|
|
87
|
+
| `figureCaption.separator` | 编号与标题之间的分隔符 | `":"` |
|
|
88
|
+
| `tableCaption.enabled` | 自动为表格编号(格式:`表 {n}`) | `true` |
|
|
89
|
+
| `tableCaption.format` | 表格编号格式 | `"表 {n}"` |
|
|
90
|
+
| `tableCaption.separator` | 编号与标题之间的分隔符 | `" "` |
|
|
91
|
+
| `renderMermaid.enabled` | 渲染 Mermaid 图表为 PNG | `true` |
|
|
92
|
+
| `renderMermaid.theme` | 图表主题 | `"tokyo-night-light"` |
|
|
93
|
+
| `renderMermaid.density` | 图片清晰度(DPI,最小 72) | `200` |
|
|
94
|
+
| `pandoc.enabled` | 生成 Word 文档 | `true` |
|
|
95
|
+
| `pandoc.outputName` | 输出文件名,`{file_name}` 代表源文件名 | `"{file_name}.docx"` |
|
|
96
|
+
|
|
97
|
+
## 样式定制
|
|
98
|
+
|
|
99
|
+
`style/style.json` 定义了 docx 输出的全部样式,包括字体、字号、颜色、缩进、间距等。
|
|
100
|
+
|
|
101
|
+
### 从模板 docx 提取样式
|
|
102
|
+
|
|
103
|
+
如果你有一个排版精美的 docx 模板,可以提取其样式:
|
|
86
104
|
|
|
87
105
|
```bash
|
|
88
|
-
bun run src/
|
|
106
|
+
bun run src/style/extract.ts
|
|
89
107
|
```
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
提取后修改 `style/style.json`,转换时会自动使用这些样式生成输出文档。
|
|
110
|
+
|
|
111
|
+
### 手动维护样式
|
|
112
|
+
|
|
113
|
+
也可直接编辑 `style/style.json`。支持以下样式配置:
|
|
114
|
+
|
|
115
|
+
- **文档默认样式**(`default.document`):全局字体、字号、段落间距
|
|
116
|
+
- **内置样式覆盖**(`default.heading1` … `default.heading6`、`default.title` 等)
|
|
117
|
+
- **自定义段落样式**(`paragraphStyles`)
|
|
118
|
+
- **自定义字符样式**(`characterStyles`)
|
|
119
|
+
|
|
120
|
+
详细的样式字段定义参考 [docx 包文档](https://docx.js.org)。
|
|
92
121
|
|
|
93
122
|
## 处理流水线
|
|
94
123
|
|
|
124
|
+
项目遵循单向处理流水线,每个步骤职责单一。
|
|
125
|
+
|
|
95
126
|
```
|
|
96
127
|
Markdown
|
|
97
128
|
↓
|
|
98
129
|
解析 AST
|
|
99
130
|
↓
|
|
100
|
-
addTitle()
|
|
131
|
+
addTitle() ← 提取文档标题
|
|
101
132
|
↓
|
|
102
|
-
normalizeHeadings()
|
|
133
|
+
normalizeHeadings() ← 修正标题层级(最小深度 → H1,消除跳跃)
|
|
103
134
|
↓
|
|
104
|
-
numberHeadings()
|
|
135
|
+
numberHeadings() ← 添加标题编号(1 / 1.1 / 1.1.1 …)
|
|
105
136
|
↓
|
|
106
|
-
numberTables()
|
|
137
|
+
numberTables() ← 添加表格编号(表 1、表 2 …)
|
|
107
138
|
↓
|
|
108
|
-
renderMermaid()
|
|
139
|
+
renderMermaid() ← Mermaid 代码块 → PNG 图片
|
|
109
140
|
↓
|
|
110
|
-
numberPictures()
|
|
141
|
+
numberPictures() ← 添加图片编号(图 1、图 2 …)
|
|
111
142
|
↓
|
|
112
143
|
序列化 Markdown
|
|
113
144
|
↓
|
|
114
|
-
pandoc
|
|
145
|
+
生成样式模板 docx ← 根据 style/style.json 生成 pandoc reference-doc
|
|
146
|
+
↓
|
|
147
|
+
pandoc → DOCX ← 以 --reference-doc 传入样式模板
|
|
115
148
|
```
|
|
116
149
|
|
|
117
150
|
## 开发
|
|
118
151
|
|
|
152
|
+
### 从源码运行
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git clone https://github.com/WXY-V1hZ/md2docx.git
|
|
156
|
+
cd md2docx
|
|
157
|
+
bun install
|
|
158
|
+
bun run src/index.ts docs/example.md
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 命令
|
|
162
|
+
|
|
119
163
|
```bash
|
|
120
164
|
# 测试
|
|
121
165
|
bun test
|
|
122
166
|
|
|
123
167
|
# 类型检查 + 代码检查 + 格式检查
|
|
124
168
|
bun check
|
|
169
|
+
|
|
170
|
+
# 清除缓存
|
|
171
|
+
bun run src/index.ts clean
|
|
125
172
|
```
|
|
126
173
|
|
|
127
174
|
## 许可
|
package/config/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v1hz/md2docx",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "基于 pandoc 的 Markdown 转 Word
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"converter",
|
|
7
7
|
"docx",
|
package/src/cli.ts
CHANGED
|
@@ -132,6 +132,7 @@ export function formatHelp(configOptions: ConfigOption[]): string {
|
|
|
132
132
|
const lines = [
|
|
133
133
|
"用法:",
|
|
134
134
|
" md2docx <md-path> [选项]",
|
|
135
|
+
" md2docx clean",
|
|
135
136
|
"",
|
|
136
137
|
"选项:",
|
|
137
138
|
" -o <path> 输出 docx 路径",
|
|
@@ -140,6 +141,9 @@ export function formatHelp(configOptions: ConfigOption[]): string {
|
|
|
140
141
|
" -v, --version 显示版本号",
|
|
141
142
|
" -h, --help 显示帮助",
|
|
142
143
|
"",
|
|
144
|
+
"子命令:",
|
|
145
|
+
" clean 清除 tmp/ 缓存",
|
|
146
|
+
"",
|
|
143
147
|
"配置覆盖:",
|
|
144
148
|
];
|
|
145
149
|
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
4
4
|
import { dirname, parse, resolve } from "path";
|
|
5
5
|
import { $ } from "bun";
|
|
6
6
|
|
|
7
7
|
import { applyConfigOverrides, formatHelp, getConfigOptions, parseCliArgs } from "./cli";
|
|
8
8
|
import { loadConfig } from "./config";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_PATH,
|
|
11
|
+
CONFIG_SCHEMA_PATH,
|
|
12
|
+
PKG_DIR,
|
|
13
|
+
TMP_DIR,
|
|
14
|
+
formattedMdPath,
|
|
15
|
+
preprocessDir,
|
|
16
|
+
} from "./paths";
|
|
10
17
|
import { preprocess } from "./preprocess/index";
|
|
11
18
|
import { startWebEditor } from "./web";
|
|
19
|
+
import { ensureTemplateDocx } from "./style/generate";
|
|
20
|
+
|
|
21
|
+
async function cleanCache(): Promise<number> {
|
|
22
|
+
const resolved = resolve(TMP_DIR);
|
|
23
|
+
if (existsSync(resolved)) {
|
|
24
|
+
rmSync(resolved, { recursive: true, force: true });
|
|
25
|
+
console.log(`已清除缓存:${resolved}`);
|
|
26
|
+
} else {
|
|
27
|
+
console.log("缓存目录不存在,无需清理。");
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
12
31
|
|
|
13
32
|
export async function run(args: string[]): Promise<number> {
|
|
14
33
|
try {
|
|
34
|
+
// 检查子命令
|
|
35
|
+
const subcommand = args[0];
|
|
36
|
+
if (subcommand === "clean") {
|
|
37
|
+
return await cleanCache();
|
|
38
|
+
}
|
|
15
39
|
const schema = JSON.parse(await Bun.file(CONFIG_SCHEMA_PATH).text());
|
|
16
40
|
const configOptions = getConfigOptions(schema);
|
|
17
41
|
const cli = parseCliArgs(args, configOptions);
|
|
@@ -46,10 +70,12 @@ export async function run(args: string[]): Promise<number> {
|
|
|
46
70
|
writeFileSync(mdOutput, formattedMd, "utf-8");
|
|
47
71
|
|
|
48
72
|
if (cfg.pandoc.enabled) {
|
|
73
|
+
const templatePath = await ensureTemplateDocx();
|
|
49
74
|
const configuredName = cfg.pandoc.outputName.replaceAll("{file_name}", baseName);
|
|
50
75
|
const docxOutput = resolve(cli.outputPath ?? configuredName);
|
|
51
76
|
mkdirSync(dirname(docxOutput), { recursive: true });
|
|
52
|
-
const result =
|
|
77
|
+
const result =
|
|
78
|
+
await $`pandoc ${mdOutput} -o ${docxOutput} --reference-doc=${templatePath}`.nothrow();
|
|
53
79
|
if (result.exitCode !== 0) {
|
|
54
80
|
console.error(`pandoc 转换失败 (exit code ${result.exitCode}):`, result.stderr.toString());
|
|
55
81
|
return 1;
|
package/src/paths.ts
CHANGED
|
@@ -27,11 +27,8 @@ export function formattedMdPath(baseName: string): string {
|
|
|
27
27
|
/** 样式模块目录 */
|
|
28
28
|
export const STYLE_DIR = `${TMP_DIR}/style`;
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
export const
|
|
32
|
-
|
|
33
|
-
/** 样式 JSON 文件路径(提取结果的输出 / 模板生成的输入) */
|
|
34
|
-
export const STYLE_JSON = `${STYLE_DIR}/style.json`;
|
|
30
|
+
/** 样式配置文件(相对于包安装目录),用户维护的样式定义 */
|
|
31
|
+
export const STYLE_CONFIG = resolve(PKG_DIR, "config/style.json");
|
|
35
32
|
|
|
36
33
|
/** 生成的模板 docx 文件路径 */
|
|
37
|
-
export const STYLE_TEMPLATE_DOCX = `${STYLE_DIR}/
|
|
34
|
+
export const STYLE_TEMPLATE_DOCX = `${STYLE_DIR}/style.docx`;
|
package/src/style/extract.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { DOMParser } from "@xmldom/xmldom";
|
|
3
3
|
import { useNamespaces } from "xpath";
|
|
4
4
|
import PizZip from "pizzip";
|
|
@@ -372,10 +372,3 @@ export function extractStylesFromDocx(docxPath: string): ExtractedStyles {
|
|
|
372
372
|
|
|
373
373
|
return result;
|
|
374
374
|
}
|
|
375
|
-
|
|
376
|
-
if (import.meta.main) {
|
|
377
|
-
const { STYLE_REF_DOCX, STYLE_JSON } = await import("../paths");
|
|
378
|
-
const styles = extractStylesFromDocx(STYLE_REF_DOCX);
|
|
379
|
-
writeFileSync(STYLE_JSON, JSON.stringify(styles, null, 2), "utf-8");
|
|
380
|
-
console.log(`Styles extracted from ${STYLE_REF_DOCX} to ${STYLE_JSON}`);
|
|
381
|
-
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { Document, Packer } from "docx";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { STYLE_CONFIG, STYLE_TEMPLATE_DOCX } from "../paths";
|
|
5
|
+
|
|
6
|
+
export async function generateTemplateDocx(): Promise<void> {
|
|
7
|
+
const styles = JSON.parse(readFileSync(STYLE_CONFIG, "utf-8"));
|
|
8
|
+
const doc = new Document({
|
|
9
|
+
styles: styles,
|
|
10
|
+
sections: [],
|
|
11
|
+
});
|
|
12
|
+
const buf = await Packer.toBuffer(doc);
|
|
13
|
+
mkdirSync(dirname(STYLE_TEMPLATE_DOCX), { recursive: true });
|
|
14
|
+
await Bun.write(STYLE_TEMPLATE_DOCX, buf);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let _generatePromise: Promise<void> | null = null;
|
|
18
|
+
|
|
19
|
+
export async function ensureTemplateDocx(): Promise<string> {
|
|
20
|
+
const exists = await Bun.file(STYLE_TEMPLATE_DOCX).exists();
|
|
21
|
+
if (!exists) {
|
|
22
|
+
if (_generatePromise === null) {
|
|
23
|
+
_generatePromise = generateTemplateDocx().catch((e) => {
|
|
24
|
+
_generatePromise = null; // 重置,允许下次重试
|
|
25
|
+
throw e;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
await _generatePromise;
|
|
29
|
+
}
|
|
30
|
+
return STYLE_TEMPLATE_DOCX;
|
|
31
|
+
}
|