@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 CHANGED
@@ -1,62 +1,67 @@
1
- # md2docx
1
+ <p align="center">
2
+ <img src="assets/logo.svg" alt="md2docx" width="160" height="160">
3
+ </p>
2
4
 
3
- 基于 pandoc 的 Markdown 转 Word (.docx) 工具,在转换前执行 AST 变换以提升文档输出质量。
5
+ <h1 align="center">md2docx</h1>
4
6
 
5
- ## 功能
7
+ <p align="center">
8
+ 基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。
9
+ </p>
6
10
 
7
- Markdown 转换为格式精美的 Word 文档,自动处理:
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
- - **文档标题** — 从 frontmatter、H1 或文件名自动提取文档标题
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
- - [Bun](https://bun.sh) 1.3
22
- - [pandoc](https://pandoc.org)(可选,需导出 Word 时安装)
20
+ Markdown 转换为格式精美的 Word 文档,自动处理:
23
21
 
24
- ### 通过 npx(推荐,无需安装)
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
- ```bash
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 README.md
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
- npx @v1hz/md2docx docs/example.md
49
+ md2docx docs/example.md
51
50
 
52
51
  # 只预处理,不调用 pandoc
53
- npx @v1hz/md2docx docs/example.md --pandoc.enabled false
52
+ md2docx docs/example.md --pandoc.enabled false
54
53
 
55
54
  # 指定输出路径
56
- npx @v1hz/md2docx docs/example.md -o output/example.docx
55
+ md2docx docs/example.md -o output/example.docx
56
+
57
+ # 打开 web 网页自定义配置
58
+ md2docx --web
57
59
 
58
60
  # 使用自定义配置文件
59
- npx @v1hz/md2docx docs/example.md --config ./my-config.json
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
- npx @v1hz/md2docx docs/example.md --figureCaption.enabled false
74
+ md2docx docs/example.md --figureCaption.enabled false
70
75
  ```
71
76
 
72
- ### 配置项概览
73
-
74
- | 配置项 | 说明 | 默认值 |
75
- | --------------------------- | ----------------- | ------------ |
76
- | `detectTitle.enabled` | 自定设置文档标题 | `true` |
77
- | `detectTitle.strategy` | 标题来源策略 | `"first-h1"` |
78
- | `normalizeHeadings.enabled` | 自动修正标题层级 | `true` |
79
- | `numberHeadings.enabled` | 自动为标题编号 | `true` |
80
- | `figureCaption.enabled` | 自动为图片编号 | `true` |
81
- | `tableCaption.enabled` | 自动为表格编号 | `true` |
82
- | `renderMermaid.enabled` | 渲染 Mermaid 图表 | `true` |
83
- | `pandoc.enabled` | 生成 Word 文档 | `true` |
84
-
85
- ## Web 配置编辑器
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/web.ts
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() ← Mermaid → PNG
139
+ renderMermaid() ← Mermaid 代码块 → PNG 图片
109
140
 
110
- numberPictures() 添加图片编号
141
+ numberPictures() 添加图片编号(图 1、图 2 …)
111
142
 
112
143
  序列化 Markdown
113
144
 
114
- pandoc → DOCX
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
  ## 许可
@@ -8,7 +8,7 @@
8
8
  "tableCaption": {
9
9
  "enabled": true,
10
10
  "format": "表 {n}",
11
- "separator": ""
11
+ "separator": " "
12
12
  },
13
13
  "normalizeHeadings": {
14
14
  "enabled": true
@@ -47,7 +47,7 @@
47
47
  "separator": {
48
48
  "description": "编号与表格标题之间的分隔符",
49
49
  "type": "string",
50
- "default": ""
50
+ "default": " "
51
51
  }
52
52
  },
53
53
  "required": ["enabled", "format", "separator"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@v1hz/md2docx",
3
- "version": "1.0.5",
4
- "description": "基于 pandoc 的 Markdown 转 Word (docx) 工具,自动处理标题、编号、Mermaid 图表等",
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 { CONFIG_PATH, CONFIG_SCHEMA_PATH, PKG_DIR, formattedMdPath, preprocessDir } from "./paths";
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 = await $`pandoc ${mdOutput} -o ${docxOutput}`.nothrow();
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
- /** 参考 docx 文件路径(样式提取的输入) */
31
- export const STYLE_REF_DOCX = `${STYLE_DIR}/ref.docx`;
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}/template.docx`;
34
+ export const STYLE_TEMPLATE_DOCX = `${STYLE_DIR}/style.docx`;
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
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
+ }