@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 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`。该程序只适用于构建目标对应的操作系统和 CPU 架构。
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
+ ![](./pictures/test.png)
373
+ ```
374
+
375
+ 新版会把原始文档目录和命令运行目录传给 Pandoc 的 `--resource-path`,并优先搜索原始文档目录。如果仍然缺图,请检查路径大小写、文件是否存在,以及图片语法中是否包含错误的 URL 编码。
376
+
353
377
  ## 许可
354
378
 
355
379
  md2docx 以 [GNU GPL v3.0](LICENSE) 发布。
@@ -26,5 +26,8 @@
26
26
  "detectTitle": {
27
27
  "enabled": true,
28
28
  "strategy": "first-h1"
29
+ },
30
+ "removeThematicBreaks": {
31
+ "enabled": true
29
32
  }
30
33
  }
@@ -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.1.0",
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((resolve5, reject) => {
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) => resolve5({ exitCode: exitCode ?? 1, stderr }));
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 dirname5, parse as parse6, resolve as resolve5 } from "node:path";
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 = resolve5(dirname5(output), assetsName);
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 = resolve5(dirname5(output));
147854
- if (dirname5(assetsDir) !== parent)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v1hz/md2docx",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
5
5
  "keywords": [
6
6
  "converter",