@v1hz/md2docx 2.1.0 → 2.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 +27 -3
- package/config/config.json +3 -0
- package/config/config.schema.json +15 -1
- package/dist/index.js +27 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
| Mermaid 图表 | 使用 beautiful-mermaid 和 resvg-wasm 将 Mermaid 渲染为高 DPI PNG |
|
|
27
27
|
| Word 样式 | 根据 JSON 样式生成 Pandoc reference DOCX,也可从现有 DOCX 提取样式 |
|
|
28
28
|
| Markdown 格式化 | 可只运行预处理流水线,输出格式化后的 Markdown |
|
|
29
|
+
| 去除分隔符 | 默认移除 `---`、`***`、`___` 等分隔符行,减少无用页面间距 |
|
|
29
30
|
| 集中缓存 | 中间文件统一存储到 `~/.md2docx/`,不会在当前目录创建 `tmp/` |
|
|
30
|
-
| Node 与可执行版本 | 支持 npm CLI,也支持构建不依赖 Node.js/Bun
|
|
31
|
+
| Node 与可执行版本 | 支持 npm CLI,也支持构建不依赖 Node.js/Bun 的 Windows 可执行文件 |
|
|
31
32
|
|
|
32
33
|
## 安装方式
|
|
33
34
|
|
|
@@ -178,6 +179,8 @@ md2docx export style -f template.docx → ./template_style.json
|
|
|
178
179
|
|
|
179
180
|
输出 DOCX 和显式导出的配置、样式仍写到用户指定位置或当前工作目录,不会写入缓存目录。
|
|
180
181
|
|
|
182
|
+
虽然 Pandoc 实际读取的是缓存中的格式化 Markdown,但本地相对资源始终优先相对于**原始 Markdown 所在目录**解析;找不到时再搜索命令运行目录。例如 `C:/docs/example.md` 中的 `./pictures/test.png` 会解析为 `C:/docs/pictures/test.png`,与从哪个目录执行 `md2docx` 无关。绝对路径、HTTP(S) URL 和 Mermaid 生成的缓存图片不受影响。
|
|
183
|
+
|
|
181
184
|
## 配置
|
|
182
185
|
|
|
183
186
|
内置配置来自 `config/config.json`,并由 `config/config.schema.json` 提供 JSON Schema。推荐先导出再编辑:
|
|
@@ -206,6 +209,7 @@ CLI 不支持覆盖单个配置项,所有配置都通过 JSON 文件管理。
|
|
|
206
209
|
| `renderMermaid.enabled` | 将 Mermaid 渲染为 PNG | `true` |
|
|
207
210
|
| `renderMermaid.theme` | beautiful-mermaid 主题 | `"tokyo-night-light"` |
|
|
208
211
|
| `renderMermaid.density` | PNG 输出 DPI,最小值 72 | `200` |
|
|
212
|
+
| `removeThematicBreaks.enabled` | 移除 `---`、`***`、`___` 等分隔符行 | `true` |
|
|
209
213
|
|
|
210
214
|
## 样式定制
|
|
211
215
|
|
|
@@ -266,6 +270,8 @@ Markdown
|
|
|
266
270
|
↓
|
|
267
271
|
addTitle()
|
|
268
272
|
↓
|
|
273
|
+
removeThematicBreaks()
|
|
274
|
+
↓
|
|
269
275
|
normalizeHeadings()
|
|
270
276
|
↓
|
|
271
277
|
numberHeadings()
|
|
@@ -285,6 +291,7 @@ Pandoc + Lua filter
|
|
|
285
291
|
DOCX
|
|
286
292
|
```
|
|
287
293
|
|
|
294
|
+
`removeThematicBreaks()` 在标题处理之前执行,避免分隔符干扰文档结构。
|
|
288
295
|
`numberTables()` 必须先于 Mermaid 渲染;`numberPictures()` 必须后于 Mermaid 渲染,这样 Mermaid 生成的图片也能获得图题。
|
|
289
296
|
|
|
290
297
|
## 构建
|
|
@@ -320,13 +327,13 @@ dist/
|
|
|
320
327
|
|
|
321
328
|
依赖会打包进 `index.js`,resvg WASM 作为相邻资源输出。`prepack` 会自动执行此构建。
|
|
322
329
|
|
|
323
|
-
###
|
|
330
|
+
### Windows 可执行文件
|
|
324
331
|
|
|
325
332
|
```bash
|
|
326
333
|
bun run build:exe
|
|
327
334
|
```
|
|
328
335
|
|
|
329
|
-
Windows 输出为 `dist/md2docx.exe
|
|
336
|
+
Windows 输出为 `dist/md2docx.exe`。
|
|
330
337
|
|
|
331
338
|
`build` 和 `build:exe` 都会先删除整个 `dist/`,因此两种产物不会同时保留。尤其不要依赖手工生成的 EXE 参与 `npm publish`:发布时 `prepack` 会重新生成 npm 所需的 `index.js` 和 WASM。
|
|
332
339
|
|
|
@@ -350,6 +357,23 @@ pandoc --version
|
|
|
350
357
|
md2docx -f report.md --force
|
|
351
358
|
```
|
|
352
359
|
|
|
360
|
+
### 从其他目录转换时图片缺失
|
|
361
|
+
|
|
362
|
+
相对图片路径应以原始 Markdown 文件为基准:
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
docs/
|
|
366
|
+
├── example.md
|
|
367
|
+
└── pictures/
|
|
368
|
+
└── test.png
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```markdown
|
|
372
|
+

|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
新版会把原始文档目录和命令运行目录传给 Pandoc 的 `--resource-path`,并优先搜索原始文档目录。如果仍然缺图,请检查路径大小写、文件是否存在,以及图片语法中是否包含错误的 URL 编码。
|
|
376
|
+
|
|
353
377
|
## 许可
|
|
354
378
|
|
|
355
379
|
md2docx 以 [GNU GPL v3.0](LICENSE) 发布。
|
package/config/config.json
CHANGED
|
@@ -141,6 +141,19 @@
|
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
143
|
"required": ["enabled", "strategy"]
|
|
144
|
+
},
|
|
145
|
+
"removeThematicBreaks": {
|
|
146
|
+
"description": "分隔符",
|
|
147
|
+
"type": "object",
|
|
148
|
+
"additionalProperties": false,
|
|
149
|
+
"properties": {
|
|
150
|
+
"enabled": {
|
|
151
|
+
"description": "自动移除 markdown 中的分隔符(---, ***, ___)",
|
|
152
|
+
"type": "boolean",
|
|
153
|
+
"default": true
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"required": ["enabled"]
|
|
144
157
|
}
|
|
145
158
|
},
|
|
146
159
|
"required": [
|
|
@@ -149,6 +162,7 @@
|
|
|
149
162
|
"normalizeHeadings",
|
|
150
163
|
"numberHeadings",
|
|
151
164
|
"renderMermaid",
|
|
152
|
-
"detectTitle"
|
|
165
|
+
"detectTitle",
|
|
166
|
+
"removeThematicBreaks"
|
|
153
167
|
]
|
|
154
168
|
}
|
package/dist/index.js
CHANGED
|
@@ -128401,7 +128401,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
128401
128401
|
// package.json
|
|
128402
128402
|
var package_default = {
|
|
128403
128403
|
name: "@v1hz/md2docx",
|
|
128404
|
-
version: "2.
|
|
128404
|
+
version: "2.2.0",
|
|
128405
128405
|
description: "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
|
|
128406
128406
|
keywords: [
|
|
128407
128407
|
"converter",
|
|
@@ -128553,7 +128553,7 @@ function cleanIntermediateFiles(paths = { targetDir: TMP_DIR, homeDir: homedir2(
|
|
|
128553
128553
|
// src/commands/convert.ts
|
|
128554
128554
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
128555
128555
|
import { spawn } from "node:child_process";
|
|
128556
|
-
import { parse as parse4 } from "node:path";
|
|
128556
|
+
import { dirname as dirname5, parse as parse4, resolve as resolve5 } from "node:path";
|
|
128557
128557
|
|
|
128558
128558
|
// src/config.ts
|
|
128559
128559
|
import { access, readFile } from "node:fs/promises";
|
|
@@ -128595,6 +128595,7 @@ function validateConfig(value, path2) {
|
|
|
128595
128595
|
if (!Number.isInteger(mermaid.density) || mermaid.density < 72) {
|
|
128596
128596
|
invalidConfig(path2, "renderMermaid.density", "必须是不小于 72 的整数");
|
|
128597
128597
|
}
|
|
128598
|
+
validateEnabled(root.removeThematicBreaks, path2, "removeThematicBreaks");
|
|
128598
128599
|
const title = expectRecord(root.detectTitle, path2, "detectTitle");
|
|
128599
128600
|
expectBoolean(title.enabled, path2, "detectTitle.enabled");
|
|
128600
128601
|
const strategy = expectString(title.strategy, path2, "detectTitle.strategy");
|
|
@@ -146041,6 +146042,11 @@ function expandHexColor(color2) {
|
|
|
146041
146042
|
return color2.length === 4 ? `#${color2[1]}${color2[1]}${color2[2]}${color2[2]}${color2[3]}${color2[3]}` : color2;
|
|
146042
146043
|
}
|
|
146043
146044
|
|
|
146045
|
+
// src/preprocess/thematic-break.ts
|
|
146046
|
+
function removeThematicBreaks(root2) {
|
|
146047
|
+
root2.children = root2.children.filter((node2) => node2.type !== "thematicBreak");
|
|
146048
|
+
}
|
|
146049
|
+
|
|
146044
146050
|
// src/preprocess/index.ts
|
|
146045
146051
|
async function preprocess2(mdPath, cfg, outDir, assetUrlDir = outDir) {
|
|
146046
146052
|
const md = await readFile3(mdPath, "utf-8");
|
|
@@ -146051,6 +146057,9 @@ async function preprocess2(mdPath, cfg, outDir, assetUrlDir = outDir) {
|
|
|
146051
146057
|
headings.push(heading2);
|
|
146052
146058
|
});
|
|
146053
146059
|
addTitle(mdPath, ast, headings, cfg.detectTitle);
|
|
146060
|
+
if (cfg.removeThematicBreaks.enabled) {
|
|
146061
|
+
removeThematicBreaks(ast);
|
|
146062
|
+
}
|
|
146054
146063
|
if (cfg.normalizeHeadings.enabled || cfg.numberHeadings.enabled) {
|
|
146055
146064
|
normalizeHeadings(headings);
|
|
146056
146065
|
}
|
|
@@ -146102,6 +146111,9 @@ var config_default = `{
|
|
|
146102
146111
|
"detectTitle": {
|
|
146103
146112
|
"enabled": true,
|
|
146104
146113
|
"strategy": "first-h1"
|
|
146114
|
+
},
|
|
146115
|
+
"removeThematicBreaks": {
|
|
146116
|
+
"enabled": true
|
|
146105
146117
|
}
|
|
146106
146118
|
}
|
|
146107
146119
|
`;
|
|
@@ -147166,6 +147178,7 @@ async function convertMarkdown(options) {
|
|
|
147166
147178
|
markdownOutput,
|
|
147167
147179
|
"-o",
|
|
147168
147180
|
output,
|
|
147181
|
+
...buildPandocResourcePathArgs(input),
|
|
147169
147182
|
`--reference-doc=${templatePath}`,
|
|
147170
147183
|
...existsSync6(luaFilter) ? [`--lua-filter=${luaFilter}`] : []
|
|
147171
147184
|
];
|
|
@@ -147175,8 +147188,14 @@ async function convertMarkdown(options) {
|
|
|
147175
147188
|
}
|
|
147176
147189
|
console.log(`已生成:${output}`);
|
|
147177
147190
|
}
|
|
147191
|
+
function buildPandocResourcePathArgs(inputPath, workingDirectory = process.cwd()) {
|
|
147192
|
+
const workingDir = resolve5(workingDirectory);
|
|
147193
|
+
const sourceDir = dirname5(resolve5(inputPath));
|
|
147194
|
+
const searchDirs = workingDir === sourceDir ? [sourceDir] : [workingDir, sourceDir];
|
|
147195
|
+
return searchDirs.map((directory) => `--resource-path=${directory}`);
|
|
147196
|
+
}
|
|
147178
147197
|
function runProcess(command, args) {
|
|
147179
|
-
return new Promise((
|
|
147198
|
+
return new Promise((resolve6, reject) => {
|
|
147180
147199
|
const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
147181
147200
|
let stderr = "";
|
|
147182
147201
|
child.stderr.setEncoding("utf-8");
|
|
@@ -147184,7 +147203,7 @@ function runProcess(command, args) {
|
|
|
147184
147203
|
stderr += chunk;
|
|
147185
147204
|
});
|
|
147186
147205
|
child.once("error", reject);
|
|
147187
|
-
child.once("close", (exitCode) =>
|
|
147206
|
+
child.once("close", (exitCode) => resolve6({ exitCode: exitCode ?? 1, stderr }));
|
|
147188
147207
|
});
|
|
147189
147208
|
}
|
|
147190
147209
|
|
|
@@ -147833,7 +147852,7 @@ async function exportStyle(options) {
|
|
|
147833
147852
|
|
|
147834
147853
|
// src/commands/format.ts
|
|
147835
147854
|
import { existsSync as existsSync7, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
147836
|
-
import { basename, dirname as
|
|
147855
|
+
import { basename, dirname as dirname6, parse as parse6, resolve as resolve6 } from "node:path";
|
|
147837
147856
|
async function formatMarkdown(options) {
|
|
147838
147857
|
const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
|
|
147839
147858
|
const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : materializeDefaultConfig();
|
|
@@ -147844,14 +147863,14 @@ async function formatMarkdown(options) {
|
|
|
147844
147863
|
const config = await loadConfig(configPath);
|
|
147845
147864
|
const outputName = parse6(output).name;
|
|
147846
147865
|
const assetsName = `${outputName}_assets`;
|
|
147847
|
-
const assetsDir =
|
|
147866
|
+
const assetsDir = resolve6(dirname6(output), assetsName);
|
|
147848
147867
|
if (config.renderMermaid.enabled && existsSync7(assetsDir)) {
|
|
147849
147868
|
if (!force) {
|
|
147850
147869
|
throw new Error(`资源目录已存在:${assetsDir}
|
|
147851
147870
|
使用 --force 覆盖现有目录`);
|
|
147852
147871
|
}
|
|
147853
|
-
const parent =
|
|
147854
|
-
if (
|
|
147872
|
+
const parent = resolve6(dirname6(output));
|
|
147873
|
+
if (dirname6(assetsDir) !== parent)
|
|
147855
147874
|
throw new Error(`拒绝清理非输出目录:${assetsDir}`);
|
|
147856
147875
|
rmSync2(assetsDir, { recursive: true, force: true });
|
|
147857
147876
|
}
|