@v1hz/md2docx 2.3.0 → 2.5.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
@@ -23,6 +23,7 @@
23
23
  | 标题层级归一化 | 将最浅标题归一化为 H1,并修复标题层级跳跃 |
24
24
  | 标题编号 | 生成 `1`、`1.1`、`1.1.1` 等编号,并可剥离常见的已有中英文编号 |
25
25
  | 表格与图片编号 | 自动生成“表 1”“图 1:标题”等题注 |
26
+ | 图片尺寸限制 | 按自然尺寸等比缩小超过最大宽度或高度的图片 |
26
27
  | Mermaid 图表 | 使用 beautiful-mermaid 和 resvg-wasm 将 Mermaid 渲染为高 DPI PNG |
27
28
  | Word 样式 | 支持受控语义化配置和完整底层样式,也可从现有 DOCX 提取样式 |
28
29
  | Markdown 格式化 | 可只运行预处理流水线,输出格式化后的 Markdown |
@@ -37,7 +38,7 @@
37
38
  前置依赖:
38
39
 
39
40
  - [Node.js 22.12+](https://nodejs.org/)
40
- - [Pandoc](https://pandoc.org/installing.html),并确保 `pandoc` 可通过 `PATH` 调用
41
+ - [Pandoc](https://pandoc.org/installing.html),并确保 `pandoc` 可通过 `PATH` 调用;图片尺寸限制需要 Pandoc 3.1.13+
41
42
 
42
43
  ```bash
43
44
  npm install -g @v1hz/md2docx
@@ -58,26 +59,27 @@ npx @v1hz/md2docx report.md
58
59
  md2docx report.md
59
60
 
60
61
  # 使用任何转换选项时,输入必须通过 --file 指定
61
- md2docx --file report.md --output output/report.docx --force
62
+ md2docx --file report.md --output output/report.docx
62
63
 
63
- # 使用自定义配置和样式
64
- md2docx -f report.md -c config.json -s style.json
64
+ # 使用自定义配置、底层样式和语义化样式配置
65
+ md2docx -f report.md -c config.json --style-raw style-raw.json --style-config style-config.json
65
66
 
66
67
  # 只执行 Markdown 预处理
67
68
  md2docx format -f report.md
68
69
 
69
- # 导出内置默认配置和样式
70
+ # 导出内置默认配置、底层样式和语义化样式配置
70
71
  md2docx export config
71
- md2docx export style
72
+ md2docx export style-raw
73
+ md2docx export style-config
72
74
 
73
75
  # 从现有 DOCX 提取样式
74
- md2docx export style -f template.docx
76
+ md2docx export style-raw -f template.docx
75
77
 
76
78
  # 删除 ~/.md2docx 中的中间文件和缓存
77
79
  md2docx clean
78
80
  ```
79
81
 
80
- 所有写文件命令默认拒绝覆盖已有文件。确认覆盖时显式传入 `--force`。
82
+ 所有写文件命令默认覆盖已有输出。
81
83
 
82
84
  ## CLI 参考
83
85
 
@@ -86,29 +88,30 @@ md2docx <markdown>
86
88
  md2docx -f <markdown> [转换选项]
87
89
  md2docx format -f <markdown> [选项]
88
90
  md2docx export config [选项]
89
- md2docx export style [选项]
91
+ md2docx export style-raw [选项]
92
+ md2docx export style-config [选项]
90
93
  md2docx clean
91
94
  ```
92
95
 
93
96
  ### 转换
94
97
 
95
- | 参数 | 说明 |
96
- | --------------------- | ---------------------------------------- |
97
- | `<markdown>` | 位置参数;仅在没有其他转换选项时允许使用 |
98
- | `-f, --file <path>` | Markdown 输入文件 |
99
- | `-c, --config <path>` | 自定义配置 JSON |
100
- | `-s, --style <path>` | 语义化样式配置或完整底层样式 JSON |
101
- | `-o, --output <path>` | DOCX 输出路径 |
102
- | `--force` | 覆盖已有输出 |
103
- | `-h, --help` | 显示帮助 |
104
- | `-v, --version` | 显示版本号 |
98
+ | 参数 | 说明 |
99
+ | ----------------------- | ---------------------------------------- |
100
+ | `<markdown>` | 位置参数;仅在没有其他转换选项时允许使用 |
101
+ | `-f, --file <path>` | Markdown 输入文件 |
102
+ | `-c, --config <path>` | 自定义配置 JSON |
103
+ | `--style-raw <path>` | 完整底层 Word 样式 JSON |
104
+ | `--style-config <path>` | 受控语义化样式配置 JSON |
105
+ | `-o, --output <path>` | DOCX 输出路径 |
106
+ | `-h, --help` | 显示帮助 |
107
+ | `-v, --version` | 显示版本号 |
105
108
 
106
- 位置参数不能和 `--file`、`--config`、`--style`、`--output` 或 `--force` 混用。例如:
109
+ 位置参数不能和 `--file`、`--config`、`--style-raw`、`--style-config` 或 `--output` 混用。例如:
107
110
 
108
111
  ```bash
109
112
  md2docx report.md # 正确
110
- md2docx report.md --force # 错误
111
- md2docx -f report.md --force # 正确
113
+ md2docx report.md -o report.docx # 错误
114
+ md2docx -f report.md -o report.docx # 正确
112
115
  ```
113
116
 
114
117
  ### format
@@ -120,16 +123,16 @@ md2docx -f report.md --force # 正确
120
123
  | `-f, --file <path>` | 必填,Markdown 输入文件 |
121
124
  | `-c, --config <path>` | 自定义配置 JSON |
122
125
  | `-o, --output <path>` | 输出 Markdown,默认 `<文件名>_formatted.md` |
123
- | `--force` | 覆盖已有输出 |
124
126
 
125
127
  ### export
126
128
 
127
129
  ```bash
128
- md2docx export config [-o config.json] [--force]
129
- md2docx export style [-f template.docx] [-o style.json] [--force]
130
+ md2docx export config [-o config.json]
131
+ md2docx export style-raw [-f template.docx] [-o style-raw.json]
132
+ md2docx export style-config [-o style-config.json]
130
133
  ```
131
134
 
132
- `export config` 导出内置默认配置。`export style` 不带 `--file` 时导出内置默认样式;指定 DOCX 时从该文档提取样式。
135
+ `export config` 导出内置 Markdown 处理配置。`export style-raw` 不带 `--file` 时导出内置底层 Word 样式;指定 DOCX 时从该文档提取底层样式。`export style-config` 导出默认语义化样式配置。
133
136
 
134
137
  ### clean
135
138
 
@@ -153,8 +156,9 @@ md2docx report.md → ./report.docx
153
156
  md2docx -f report.md → ./report.docx
154
157
  md2docx format -f report.md → ./report_formatted.md
155
158
  md2docx export config → ./config.json
156
- md2docx export style → ./style.json
157
- md2docx export style -f template.docx → ./template_style.json
159
+ md2docx export style-raw → ./style-raw.json
160
+ md2docx export style-raw -f template.docx → ./template_style-raw.json
161
+ md2docx export style-config → ./style-config.json
158
162
  ```
159
163
 
160
164
  ## 中间文件与缓存
@@ -170,8 +174,9 @@ md2docx export style -f template.docx → ./template_style.json
170
174
  ├── resources/
171
175
  │ ├── config.json
172
176
  │ ├── style-config.json
173
- │ ├── style.json
174
- └── add-inline-code.lua
177
+ │ ├── style-raw.json
178
+ ├── add-inline-code.lua
179
+ │ └── limit-image-size.lua
175
180
  └── style/
176
181
  └── <样式内容哈希>.docx
177
182
  ```
@@ -210,26 +215,51 @@ CLI 不支持覆盖单个配置项,所有配置都通过 JSON 文件管理。
210
215
  | `renderMermaid.enabled` | 将 Mermaid 渲染为 PNG | `true` |
211
216
  | `renderMermaid.theme` | beautiful-mermaid 主题 | `"tokyo-night-light"` |
212
217
  | `renderMermaid.density` | PNG 输出 DPI,最小值 72 | `200` |
218
+ | `imageSize.enabled` | 等比缩小超过尺寸限制的图片 | `true` |
219
+ | `imageSize.maxWidthCm` | DOCX 图片最大宽度(厘米) | `12` |
220
+ | `imageSize.maxHeightCm` | DOCX 图片最大高度(厘米) | `12` |
213
221
  | `removeThematicBreaks.enabled` | 移除 `---`、`***`、`___` 等分隔符行 | `true` |
214
222
 
223
+ 图片尺寸限制在 Pandoc 生成 DOCX 前通过 Lua filter 应用。程序读取图片像素尺寸和 DPI,仅缩小超限图片,不会放大小图;宽度和高度使用同一缩放比例。Markdown 中已经显式设置 `width` 或 `height` 的图片视为用户覆盖,不应用全局限制。单张图片无法读取尺寸时会输出警告并继续转换。
224
+
215
225
  ## 样式定制
216
226
 
217
227
  普通用户推荐使用受控的语义化样式配置,只修改程序明确开放的高频选项。其余颜色、尺寸、对齐方式、间距和 Word 样式继承关系继续由内置预设管理。
218
228
 
219
- 仓库中的 `config/style-config.json` 是可直接复制和修改的默认配置,`config/style-config.schema.json` 用于编辑器提示和校验。未指定 `--style` 时,转换会自动加载内嵌的默认 `style-config.json`,再将它编译到内置底层样式;不需要手工传入 `-s`。
229
+ 仓库中的 `config/style-config.json` 是可直接复制和修改的默认配置,`config/style-config.schema.json` 用于编辑器提示和校验。完整底层 Word 样式位于 `config/style-raw.json`。
220
230
 
221
231
  ```json
222
232
  {
223
233
  "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
224
234
  "schemaVersion": 1,
225
- "preset": "default",
226
235
  "options": {
227
236
  "body": {
228
- "firstLineIndent": false
237
+ "firstLineIndent": false,
238
+ "lineSpacing": "onePointFive"
229
239
  },
230
240
  "headings": {
231
241
  "1": {
232
- "startOnNewPage": false
242
+ "startOnNewPage": false,
243
+ "alignment": "left",
244
+ "bold": true
245
+ },
246
+ "2": {
247
+ "bold": true
248
+ },
249
+ "3": {
250
+ "bold": true
251
+ },
252
+ "4": {
253
+ "bold": true,
254
+ "italic": false
255
+ },
256
+ "5": {
257
+ "bold": true,
258
+ "italic": false
259
+ },
260
+ "6": {
261
+ "bold": true,
262
+ "italic": false
233
263
  }
234
264
  },
235
265
  "inlineCode": {
@@ -245,18 +275,27 @@ CLI 不支持覆盖单个配置项,所有配置都通过 JSON 文件管理。
245
275
  将配置保存为 `style-config.json` 后使用:
246
276
 
247
277
  ```bash
248
- md2docx -f report.md -s style-config.json
278
+ md2docx -f report.md --style-config style-config.json
249
279
  ```
250
280
 
251
- 字段缺失表示继承预设;`true` 表示使用预设效果;`false` 表示显式关闭。当前只开放正文首行缩进、一级标题另起一页、行内代码背景和代码块外框。完整设计见 [`docs/style-config-design.md`](docs/style-config-design.md)。
281
+ 字段缺失表示继承底层样式;`true` 表示显式启用完整效果;`false` 表示显式关闭。正文行距为浮点数倍数,例如 `1.5` 表示 1.5 倍行距;一级标题对齐只接受 `left`、`center`。当前还开放一至六级标题粗体,以及四至六级标题斜体。完整设计见 [`docs/style-config-design.md`](docs/style-config-design.md)。
282
+
283
+ 转换时按以下规则选择输入:
252
284
 
253
- `config/style.json` 仍定义完整的底层 DOCX 样式,包括标题、段落、字符和表格样式。转换时,项目先将语义化配置编译到底层样式,再用 `docx` 生成 reference DOCX,最后通过 Pandoc 的 `--reference-doc` 应用样式。现有完整样式 JSON 可继续直接传给 `--style`。
285
+ | 参数 | 行为 |
286
+ | ------------------- | ----------------------------------- |
287
+ | 都不指定 | 默认 raw + 默认 config |
288
+ | 仅 `--style-raw` | 直接使用用户 raw,不应用默认 config |
289
+ | 仅 `--style-config` | 用户 config 应用到默认 raw |
290
+ | 两者都指定 | 用户 config 应用到用户 raw |
291
+
292
+ 最终有效的底层样式用于生成 reference DOCX,并通过 Pandoc 的 `--reference-doc` 应用。`--style-raw` 和 `--style-config` 类型固定,不能互换。
254
293
 
255
294
  ### 从现有 DOCX 提取
256
295
 
257
296
  ```bash
258
- md2docx export style -f template.docx
259
- md2docx -f report.md -s template_style.json
297
+ md2docx export style-raw -f template.docx
298
+ md2docx -f report.md --style-raw template_style-raw.json
260
299
  ```
261
300
 
262
301
  ### 高级手动维护
@@ -323,7 +362,7 @@ numberPictures()
323
362
 
324
363
  生成或复用 reference DOCX
325
364
 
326
- Pandoc + Lua filter
365
+ Pandoc + 行内代码/图片尺寸 Lua filter
327
366
 
328
367
  DOCX
329
368
  ```
@@ -386,14 +425,6 @@ pandoc --version
386
425
 
387
426
  如果命令不存在,请从 [Pandoc 官方安装页](https://pandoc.org/installing.html) 安装,并重新打开终端使 `PATH` 生效。
388
427
 
389
- ### 输出文件已存在
390
-
391
- 默认不会覆盖文件。确认目标可以覆盖后使用:
392
-
393
- ```bash
394
- md2docx -f report.md --force
395
- ```
396
-
397
428
  ### 从其他目录转换时图片缺失
398
429
 
399
430
  相对图片路径应以原始 Markdown 文件为基准:
@@ -23,6 +23,11 @@
23
23
  "theme": "tokyo-night-light",
24
24
  "density": 200
25
25
  },
26
+ "imageSize": {
27
+ "enabled": true,
28
+ "maxWidthCm": 12,
29
+ "maxHeightCm": 12
30
+ },
26
31
  "detectTitle": {
27
32
  "enabled": true,
28
33
  "strategy": "first-h1"
@@ -117,6 +117,31 @@
117
117
  },
118
118
  "required": ["enabled", "theme", "density"]
119
119
  },
120
+ "imageSize": {
121
+ "description": "DOCX 图片尺寸限制",
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "properties": {
125
+ "enabled": {
126
+ "description": "按图片自然尺寸等比缩小超限图片",
127
+ "type": "boolean",
128
+ "default": true
129
+ },
130
+ "maxWidthCm": {
131
+ "description": "图片最大宽度(厘米)",
132
+ "type": "number",
133
+ "exclusiveMinimum": 0,
134
+ "default": 12
135
+ },
136
+ "maxHeightCm": {
137
+ "description": "图片最大高度(厘米)",
138
+ "type": "number",
139
+ "exclusiveMinimum": 0,
140
+ "default": 12
141
+ }
142
+ },
143
+ "required": ["enabled", "maxWidthCm", "maxHeightCm"]
144
+ },
120
145
  "detectTitle": {
121
146
  "description": "文档标题",
122
147
  "type": "object",
@@ -162,6 +187,7 @@
162
187
  "normalizeHeadings",
163
188
  "numberHeadings",
164
189
  "renderMermaid",
190
+ "imageSize",
165
191
  "detectTitle",
166
192
  "removeThematicBreaks"
167
193
  ]
@@ -0,0 +1,93 @@
1
+ local max_width_cm
2
+ local max_height_cm
3
+ local size_cache = {}
4
+ local warned = {}
5
+
6
+ local function warn_once(source, message)
7
+ if warned[source] then
8
+ return
9
+ end
10
+ warned[source] = true
11
+ local text = "无法限制图片尺寸 " .. source .. ":" .. message
12
+ if pandoc.log and pandoc.log.warn then
13
+ pandoc.log.warn(text)
14
+ else
15
+ io.stderr:write("[WARNING] " .. text .. "\n")
16
+ end
17
+ end
18
+
19
+ local function metadata_number(meta, key)
20
+ local value = meta[key]
21
+ if value == nil then
22
+ return nil
23
+ end
24
+ return tonumber(pandoc.utils.stringify(value))
25
+ end
26
+
27
+ local function read_config(meta)
28
+ max_width_cm = metadata_number(meta, "md2docx-image-max-width-cm")
29
+ max_height_cm = metadata_number(meta, "md2docx-image-max-height-cm")
30
+ end
31
+
32
+ local function image_size(source)
33
+ local cached = size_cache[source]
34
+ if cached ~= nil then
35
+ return cached or nil
36
+ end
37
+ if pandoc.image == nil or pandoc.image.size == nil then
38
+ warn_once(source, "当前 Pandoc 不支持 pandoc.image.size,请升级到 3.1.13 或更高版本")
39
+ size_cache[source] = false
40
+ return nil
41
+ end
42
+
43
+ local fetched, _, contents = pcall(pandoc.mediabag.fetch, source)
44
+ if not fetched or contents == nil then
45
+ warn_once(source, fetched and "未读取到图片内容" or tostring(_))
46
+ size_cache[source] = false
47
+ return nil
48
+ end
49
+
50
+ local measured, size = pcall(pandoc.image.size, contents, PANDOC_WRITER_OPTIONS)
51
+ if not measured then
52
+ warn_once(source, tostring(size))
53
+ size_cache[source] = false
54
+ return nil
55
+ end
56
+
57
+ size_cache[source] = size
58
+ return size
59
+ end
60
+
61
+ local function limit_image(image)
62
+ if max_width_cm == nil or max_height_cm == nil then
63
+ return nil
64
+ end
65
+ if image.attributes.width ~= nil or image.attributes.height ~= nil then
66
+ return nil
67
+ end
68
+
69
+ local size = image_size(image.src)
70
+ if size == nil then
71
+ return nil
72
+ end
73
+
74
+ local fallback_dpi = PANDOC_WRITER_OPTIONS.dpi or 96
75
+ local horizontal_dpi = size.dpi_horz > 0 and size.dpi_horz or fallback_dpi
76
+ local vertical_dpi = size.dpi_vert > 0 and size.dpi_vert or fallback_dpi
77
+ local width_cm = size.width / horizontal_dpi * 2.54
78
+ local height_cm = size.height / vertical_dpi * 2.54
79
+ local scale = math.min(1, max_width_cm / width_cm, max_height_cm / height_cm)
80
+
81
+ if scale >= 1 then
82
+ return nil
83
+ end
84
+
85
+ image.attributes.width = string.format("%.4fcm", width_cm * scale)
86
+ image.attributes.height = string.format("%.4fcm", height_cm * scale)
87
+ return image
88
+ end
89
+
90
+ return {
91
+ { Meta = read_config },
92
+ { Image = limit_image },
93
+ }
@@ -1,14 +1,34 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
3
3
  "schemaVersion": 1,
4
- "preset": "default",
5
4
  "options": {
6
5
  "body": {
7
- "firstLineIndent": true
6
+ "firstLineIndent": true,
7
+ "lineSpacing": 1.5
8
8
  },
9
9
  "headings": {
10
10
  "1": {
11
- "startOnNewPage": false
11
+ "startOnNewPage": false,
12
+ "alignment": "left",
13
+ "bold": true
14
+ },
15
+ "2": {
16
+ "bold": true
17
+ },
18
+ "3": {
19
+ "bold": true
20
+ },
21
+ "4": {
22
+ "bold": true,
23
+ "italic": false
24
+ },
25
+ "5": {
26
+ "bold": true,
27
+ "italic": false
28
+ },
29
+ "6": {
30
+ "bold": true,
31
+ "italic": false
12
32
  }
13
33
  },
14
34
  "inlineCode": {
@@ -4,7 +4,7 @@
4
4
  "title": "md2docx 语义化样式配置",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["schemaVersion", "preset"],
7
+ "required": ["schemaVersion"],
8
8
  "properties": {
9
9
  "$schema": {
10
10
  "type": "string"
@@ -12,9 +12,6 @@
12
12
  "schemaVersion": {
13
13
  "const": 1
14
14
  },
15
- "preset": {
16
- "const": "default"
17
- },
18
15
  "options": {
19
16
  "type": "object",
20
17
  "additionalProperties": false,
@@ -26,6 +23,11 @@
26
23
  "firstLineIndent": {
27
24
  "type": "boolean",
28
25
  "description": "正文是否使用预设的首行缩进"
26
+ },
27
+ "lineSpacing": {
28
+ "type": "number",
29
+ "exclusiveMinimum": 0,
30
+ "description": "正文行距倍数,如 1.5 表示 1.5 倍行距"
29
31
  }
30
32
  }
31
33
  },
@@ -40,8 +42,32 @@
40
42
  "startOnNewPage": {
41
43
  "type": "boolean",
42
44
  "description": "一级标题是否另起一页"
45
+ },
46
+ "alignment": {
47
+ "type": "string",
48
+ "enum": ["left", "center"],
49
+ "description": "一级标题左对齐或居中"
50
+ },
51
+ "bold": {
52
+ "type": "boolean",
53
+ "description": "一级标题是否加粗"
43
54
  }
44
55
  }
56
+ },
57
+ "2": {
58
+ "$ref": "#/$defs/headingBold"
59
+ },
60
+ "3": {
61
+ "$ref": "#/$defs/headingBold"
62
+ },
63
+ "4": {
64
+ "$ref": "#/$defs/headingBoldItalic"
65
+ },
66
+ "5": {
67
+ "$ref": "#/$defs/headingBoldItalic"
68
+ },
69
+ "6": {
70
+ "$ref": "#/$defs/headingBoldItalic"
45
71
  }
46
72
  }
47
73
  },
@@ -67,5 +93,31 @@
67
93
  }
68
94
  }
69
95
  }
96
+ },
97
+ "$defs": {
98
+ "headingBold": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "bold": {
103
+ "type": "boolean",
104
+ "description": "标题是否加粗"
105
+ }
106
+ }
107
+ },
108
+ "headingBoldItalic": {
109
+ "type": "object",
110
+ "additionalProperties": false,
111
+ "properties": {
112
+ "bold": {
113
+ "type": "boolean",
114
+ "description": "标题是否加粗"
115
+ },
116
+ "italic": {
117
+ "type": "boolean",
118
+ "description": "标题是否斜体"
119
+ }
120
+ }
121
+ }
70
122
  }
71
123
  }
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.3.0",
128404
+ version: "2.5.0",
128405
128405
  description: "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
128406
128406
  keywords: [
128407
128407
  "converter",
@@ -128471,8 +128471,8 @@ var package_default = {
128471
128471
  // src/cli.ts
128472
128472
  function createProgram(version, actions) {
128473
128473
  const program2 = new Command;
128474
- program2.name("md2docx").description("将 Markdown 转换为 Word 文档").version(version, "-v, --version", "显示版本号").helpOption("-h, --help", "显示帮助").helpCommand(false).showHelpAfterError("使用 --help 查看帮助").showSuggestionAfterError().enablePositionalOptions().argument("[markdown]", "Markdown 文件(仅无其他转换选项时)").option("-f, --file <path>", "Markdown 文件").option("-c, --config <path>", "自定义配置文件").option("-s, --style <path>", "自定义底层样式或语义化样式配置").option("-o, --output <path>", "输出 DOCX 文件").option("--force", "覆盖已有文件").action(async function(markdown, options) {
128475
- const hasAdditionalOptions = options.config !== undefined || options.style !== undefined || options.output !== undefined || options.force !== undefined;
128474
+ program2.name("md2docx").description("将 Markdown 转换为 Word 文档").version(version, "-v, --version", "显示版本号").helpOption("-h, --help", "显示帮助").helpCommand(false).showHelpAfterError("使用 --help 查看帮助").showSuggestionAfterError().enablePositionalOptions().argument("[markdown]", "Markdown 文件(仅无其他转换选项时)").option("-f, --file <path>", "Markdown 文件").option("-c, --config <path>", "自定义配置文件").option("--style-raw <path>", "自定义底层 Word 样式").option("--style-config <path>", "自定义语义化样式配置").option("-o, --output <path>", "输出 DOCX 文件").action(async function(markdown, options) {
128475
+ const hasAdditionalOptions = options.config !== undefined || options.styleRaw !== undefined || options.styleConfig !== undefined || options.output !== undefined;
128476
128476
  if (markdown !== undefined) {
128477
128477
  if (options.file !== undefined || hasAdditionalOptions) {
128478
128478
  this.error("位置参数不能与转换选项同时使用;使用选项时请通过 -f, --file 指定 Markdown 文件");
@@ -128493,11 +128493,13 @@ function createProgram(version, actions) {
128493
128493
  this.help({ error: true });
128494
128494
  });
128495
128495
  exportCommand.exitOverride();
128496
- const exportConfigCommand = exportCommand.command("config").description("导出默认配置").helpOption("-h, --help", "显示帮助").option("-o, --output <path>", "输出 JSON 文件,默认 ./config.json").option("--force", "覆盖已有文件").action(actions.exportConfig);
128496
+ const exportConfigCommand = exportCommand.command("config").description("导出默认配置").helpOption("-h, --help", "显示帮助").option("-o, --output <path>", "输出 JSON 文件,默认 ./config.json").action(actions.exportConfig);
128497
128497
  exportConfigCommand.exitOverride();
128498
- const exportStyleCommand = exportCommand.command("style").description("导出默认样式,或从 DOCX 提取样式").helpOption("-h, --help", "显示帮助").option("-f, --file <path>", "用于提取样式的 DOCX 文件").option("-o, --output <path>", "输出 JSON 文件").option("--force", "覆盖已有文件").action(actions.exportStyle);
128499
- exportStyleCommand.exitOverride();
128500
- const formatCommand = program2.command("format").description("预处理并格式化 Markdown").helpOption("-h, --help", "显示帮助").requiredOption("-f, --file <path>", "Markdown 文件").option("-c, --config <path>", "自定义配置文件").option("-o, --output <path>", "输出 Markdown 文件").option("--force", "覆盖已有文件").action(actions.format);
128498
+ const exportStyleRawCommand = exportCommand.command("style-raw").description("导出默认底层 Word 样式,或从 DOCX 提取底层样式").helpOption("-h, --help", "显示帮助").option("-f, --file <path>", "用于提取底层样式的 DOCX 文件").option("-o, --output <path>", "输出 JSON 文件").action(actions.exportStyleRaw);
128499
+ exportStyleRawCommand.exitOverride();
128500
+ const exportStyleConfigCommand = exportCommand.command("style-config").description("导出默认语义化样式配置").helpOption("-h, --help", "显示帮助").option("-o, --output <path>", "输出 JSON 文件,默认 ./style-config.json").action(actions.exportStyleConfig);
128501
+ exportStyleConfigCommand.exitOverride();
128502
+ const formatCommand = program2.command("format").description("预处理并格式化 Markdown").helpOption("-h, --help", "显示帮助").requiredOption("-f, --file <path>", "Markdown 文件").option("-c, --config <path>", "自定义配置文件").option("-o, --output <path>", "输出 Markdown 文件").action(actions.format);
128501
128503
  formatCommand.exitOverride();
128502
128504
  const cleanCommand = program2.command("clean").description("清除 ~/.md2docx 中的预处理文件和样式缓存").helpOption("-h, --help", "显示帮助").action(async () => {
128503
128505
  await actions.clean();
@@ -128595,6 +128597,10 @@ function validateConfig(value, path2) {
128595
128597
  if (!Number.isInteger(mermaid.density) || mermaid.density < 72) {
128596
128598
  invalidConfig(path2, "renderMermaid.density", "必须是不小于 72 的整数");
128597
128599
  }
128600
+ const imageSize = expectRecord(root.imageSize, path2, "imageSize");
128601
+ expectBoolean(imageSize.enabled, path2, "imageSize.enabled");
128602
+ expectPositiveNumber(imageSize.maxWidthCm, path2, "imageSize.maxWidthCm");
128603
+ expectPositiveNumber(imageSize.maxHeightCm, path2, "imageSize.maxHeightCm");
128598
128604
  validateEnabled(root.removeThematicBreaks, path2, "removeThematicBreaks");
128599
128605
  const title = expectRecord(root.detectTitle, path2, "detectTitle");
128600
128606
  expectBoolean(title.enabled, path2, "detectTitle.enabled");
@@ -128629,6 +128635,12 @@ function expectString(value, path2, field) {
128629
128635
  invalidConfig(path2, field, "必须是字符串");
128630
128636
  return value;
128631
128637
  }
128638
+ function expectPositiveNumber(value, path2, field) {
128639
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
128640
+ invalidConfig(path2, field, "必须是大于 0 的有限数字");
128641
+ }
128642
+ return value;
128643
+ }
128632
128644
  function invalidConfig(path2, field, reason) {
128633
128645
  throw new Error(`配置文件无效:${path2}
128634
128646
  位置:${field}
@@ -128652,11 +128664,7 @@ function resolveOutputPath(path2, defaultName, extensions, label) {
128652
128664
  assertExtension(resolved, extensions, label);
128653
128665
  return resolved;
128654
128666
  }
128655
- function prepareOutput(path2, force) {
128656
- if (existsSync2(path2) && !force) {
128657
- throw new Error(`输出文件已存在:${path2}
128658
- 使用 --force 覆盖现有文件`);
128659
- }
128667
+ function prepareOutput(path2) {
128660
128668
  mkdirSync(dirname2(path2), { recursive: true });
128661
128669
  }
128662
128670
  function assertExtension(path2, extensions, label) {
@@ -146108,6 +146116,11 @@ var config_default = `{
146108
146116
  "theme": "tokyo-night-light",
146109
146117
  "density": 200
146110
146118
  },
146119
+ "imageSize": {
146120
+ "enabled": true,
146121
+ "maxWidthCm": 12,
146122
+ "maxHeightCm": 12
146123
+ },
146111
146124
  "detectTitle": {
146112
146125
  "enabled": true,
146113
146126
  "strategy": "first-h1"
@@ -146118,6 +146131,102 @@ var config_default = `{
146118
146131
  }
146119
146132
  `;
146120
146133
 
146134
+ // config/lua/limit-image-size.lua
146135
+ var limit_image_size_default = `local max_width_cm
146136
+ local max_height_cm
146137
+ local size_cache = {}
146138
+ local warned = {}
146139
+
146140
+ local function warn_once(source, message)
146141
+ if warned[source] then
146142
+ return
146143
+ end
146144
+ warned[source] = true
146145
+ local text = "无法限制图片尺寸 " .. source .. ":" .. message
146146
+ if pandoc.log and pandoc.log.warn then
146147
+ pandoc.log.warn(text)
146148
+ else
146149
+ io.stderr:write("[WARNING] " .. text .. "\\n")
146150
+ end
146151
+ end
146152
+
146153
+ local function metadata_number(meta, key)
146154
+ local value = meta[key]
146155
+ if value == nil then
146156
+ return nil
146157
+ end
146158
+ return tonumber(pandoc.utils.stringify(value))
146159
+ end
146160
+
146161
+ local function read_config(meta)
146162
+ max_width_cm = metadata_number(meta, "md2docx-image-max-width-cm")
146163
+ max_height_cm = metadata_number(meta, "md2docx-image-max-height-cm")
146164
+ end
146165
+
146166
+ local function image_size(source)
146167
+ local cached = size_cache[source]
146168
+ if cached ~= nil then
146169
+ return cached or nil
146170
+ end
146171
+ if pandoc.image == nil or pandoc.image.size == nil then
146172
+ warn_once(source, "当前 Pandoc 不支持 pandoc.image.size,请升级到 3.1.13 或更高版本")
146173
+ size_cache[source] = false
146174
+ return nil
146175
+ end
146176
+
146177
+ local fetched, _, contents = pcall(pandoc.mediabag.fetch, source)
146178
+ if not fetched or contents == nil then
146179
+ warn_once(source, fetched and "未读取到图片内容" or tostring(_))
146180
+ size_cache[source] = false
146181
+ return nil
146182
+ end
146183
+
146184
+ local measured, size = pcall(pandoc.image.size, contents, PANDOC_WRITER_OPTIONS)
146185
+ if not measured then
146186
+ warn_once(source, tostring(size))
146187
+ size_cache[source] = false
146188
+ return nil
146189
+ end
146190
+
146191
+ size_cache[source] = size
146192
+ return size
146193
+ end
146194
+
146195
+ local function limit_image(image)
146196
+ if max_width_cm == nil or max_height_cm == nil then
146197
+ return nil
146198
+ end
146199
+ if image.attributes.width ~= nil or image.attributes.height ~= nil then
146200
+ return nil
146201
+ end
146202
+
146203
+ local size = image_size(image.src)
146204
+ if size == nil then
146205
+ return nil
146206
+ end
146207
+
146208
+ local fallback_dpi = PANDOC_WRITER_OPTIONS.dpi or 96
146209
+ local horizontal_dpi = size.dpi_horz > 0 and size.dpi_horz or fallback_dpi
146210
+ local vertical_dpi = size.dpi_vert > 0 and size.dpi_vert or fallback_dpi
146211
+ local width_cm = size.width / horizontal_dpi * 2.54
146212
+ local height_cm = size.height / vertical_dpi * 2.54
146213
+ local scale = math.min(1, max_width_cm / width_cm, max_height_cm / height_cm)
146214
+
146215
+ if scale >= 1 then
146216
+ return nil
146217
+ end
146218
+
146219
+ image.attributes.width = string.format("%.4fcm", width_cm * scale)
146220
+ image.attributes.height = string.format("%.4fcm", height_cm * scale)
146221
+ return image
146222
+ end
146223
+
146224
+ return {
146225
+ { Meta = read_config },
146226
+ { Image = limit_image },
146227
+ }
146228
+ `;
146229
+
146121
146230
  // config/lua/add-inline-code.lua
146122
146231
  var add_inline_code_default = `-- Author: Achuan-2
146123
146232
  function Code(el)
@@ -146126,32 +146235,8 @@ function Code(el)
146126
146235
  end
146127
146236
  return { Code = Code }`;
146128
146237
 
146129
- // config/style-config.json
146130
- var style_config_default = `{
146131
- "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
146132
- "schemaVersion": 1,
146133
- "preset": "default",
146134
- "options": {
146135
- "body": {
146136
- "firstLineIndent": true
146137
- },
146138
- "headings": {
146139
- "1": {
146140
- "startOnNewPage": false
146141
- }
146142
- },
146143
- "inlineCode": {
146144
- "background": true
146145
- },
146146
- "codeBlock": {
146147
- "border": true
146148
- }
146149
- }
146150
- }
146151
- `;
146152
-
146153
- // config/style.json
146154
- var style_default = `{
146238
+ // config/style-raw.json
146239
+ var style_raw_default = `{
146155
146240
  "default": {
146156
146241
  "document": {
146157
146242
  "run": {
@@ -147108,10 +147193,54 @@ var style_default = `{
147108
147193
  }
147109
147194
  `;
147110
147195
 
147196
+ // config/style-config.json
147197
+ var style_config_default = `{
147198
+ "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
147199
+ "schemaVersion": 1,
147200
+ "options": {
147201
+ "body": {
147202
+ "firstLineIndent": true,
147203
+ "lineSpacing": 1.5
147204
+ },
147205
+ "headings": {
147206
+ "1": {
147207
+ "startOnNewPage": false,
147208
+ "alignment": "left",
147209
+ "bold": true
147210
+ },
147211
+ "2": {
147212
+ "bold": true
147213
+ },
147214
+ "3": {
147215
+ "bold": true
147216
+ },
147217
+ "4": {
147218
+ "bold": true,
147219
+ "italic": false
147220
+ },
147221
+ "5": {
147222
+ "bold": true,
147223
+ "italic": false
147224
+ },
147225
+ "6": {
147226
+ "bold": true,
147227
+ "italic": false
147228
+ }
147229
+ },
147230
+ "inlineCode": {
147231
+ "background": true
147232
+ },
147233
+ "codeBlock": {
147234
+ "border": true
147235
+ }
147236
+ }
147237
+ }
147238
+ `;
147239
+
147111
147240
  // src/resources.ts
147112
147241
  var DEFAULT_CONFIG_TEXT = config_default;
147113
147242
  var DEFAULT_STYLE_CONFIG_TEXT = style_config_default;
147114
- var DEFAULT_STYLE_TEXT = style_default;
147243
+ var DEFAULT_STYLE_RAW_TEXT = style_raw_default;
147115
147244
  function materializeResource(name2, content3) {
147116
147245
  const directory = join4(TMP_DIR, "resources");
147117
147246
  const path3 = join4(directory, name2);
@@ -147124,41 +147253,30 @@ function materializeResource(name2, content3) {
147124
147253
  function materializeDefaultConfig() {
147125
147254
  return materializeResource("config.json", DEFAULT_CONFIG_TEXT);
147126
147255
  }
147127
- function materializeDefaultStyle() {
147128
- return materializeResource("style.json", DEFAULT_STYLE_TEXT);
147256
+ function materializeDefaultStyleRaw() {
147257
+ return materializeResource("style-raw.json", DEFAULT_STYLE_RAW_TEXT);
147129
147258
  }
147130
147259
  function materializeDefaultStyleConfig() {
147131
- materializeDefaultStyle();
147132
147260
  return materializeResource("style-config.json", DEFAULT_STYLE_CONFIG_TEXT);
147133
147261
  }
147134
- function materializeLuaFilter() {
147262
+ function materializeInlineCodeFilter() {
147135
147263
  return materializeResource("add-inline-code.lua", add_inline_code_default);
147136
147264
  }
147137
-
147138
- // src/style/generate.ts
147139
- import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
147140
- import { writeFile } from "node:fs/promises";
147141
- import { createHash as createHash2 } from "node:crypto";
147142
- import { dirname as dirname4 } from "path";
147265
+ function materializeImageSizeFilter() {
147266
+ return materializeResource("limit-image-size.lua", limit_image_size_default);
147267
+ }
147143
147268
 
147144
147269
  // src/style/compiler.ts
147145
147270
  import { readFileSync as readFileSync2 } from "node:fs";
147146
147271
 
147147
147272
  // src/style/config.ts
147148
- function isStyleConfig(value2) {
147149
- if (!isRecord(value2))
147150
- return false;
147151
- return "schemaVersion" in value2 || "preset" in value2 || "options" in value2;
147152
- }
147153
147273
  function validateStyleConfig(value2, path3) {
147154
147274
  const root2 = expectRecord2(value2, path3, "样式配置");
147155
- rejectUnknown(root2, ["$schema", "schemaVersion", "preset", "options"], path3, "样式配置");
147275
+ rejectUnknown(root2, ["$schema", "schemaVersion", "options"], path3, "样式配置");
147156
147276
  if (root2.$schema !== undefined)
147157
147277
  expectString2(root2.$schema, path3, "$schema");
147158
147278
  if (root2.schemaVersion !== 1)
147159
147279
  invalidStyleConfig(path3, "schemaVersion", "目前只支持版本 1");
147160
- if (root2.preset !== "default")
147161
- invalidStyleConfig(path3, "preset", '目前只支持 "default"');
147162
147280
  if (root2.options !== undefined)
147163
147281
  validateOptions(root2.options, path3);
147164
147282
  return root2;
@@ -147168,21 +147286,25 @@ function validateOptions(value2, path3) {
147168
147286
  rejectUnknown(options, ["body", "headings", "inlineCode", "codeBlock"], path3, "options");
147169
147287
  if (options.body !== undefined) {
147170
147288
  const body = expectRecord2(options.body, path3, "options.body");
147171
- rejectUnknown(body, ["firstLineIndent"], path3, "options.body");
147289
+ rejectUnknown(body, ["firstLineIndent", "lineSpacing"], path3, "options.body");
147172
147290
  if (body.firstLineIndent !== undefined) {
147173
147291
  expectBoolean2(body.firstLineIndent, path3, "options.body.firstLineIndent");
147174
147292
  }
147293
+ if (body.lineSpacing !== undefined) {
147294
+ if (typeof body.lineSpacing !== "number" || !Number.isFinite(body.lineSpacing) || body.lineSpacing <= 0) {
147295
+ invalidStyleConfig(path3, "options.body.lineSpacing", "必须是大于 0 的有限数字");
147296
+ }
147297
+ }
147175
147298
  }
147176
147299
  if (options.headings !== undefined) {
147177
147300
  const headings = expectRecord2(options.headings, path3, "options.headings");
147178
- rejectUnknown(headings, ["1"], path3, "options.headings");
147179
- if (headings["1"] !== undefined) {
147180
- const heading1 = expectRecord2(headings["1"], path3, 'options.headings["1"]');
147181
- rejectUnknown(heading1, ["startOnNewPage"], path3, 'options.headings["1"]');
147182
- if (heading1.startOnNewPage !== undefined) {
147183
- expectBoolean2(heading1.startOnNewPage, path3, 'options.headings["1"].startOnNewPage');
147184
- }
147185
- }
147301
+ rejectUnknown(headings, ["1", "2", "3", "4", "5", "6"], path3, "options.headings");
147302
+ validateHeading(headings, "1", ["startOnNewPage", "alignment", "bold"], path3);
147303
+ validateHeading(headings, "2", ["bold"], path3);
147304
+ validateHeading(headings, "3", ["bold"], path3);
147305
+ validateHeading(headings, "4", ["bold", "italic"], path3);
147306
+ validateHeading(headings, "5", ["bold", "italic"], path3);
147307
+ validateHeading(headings, "6", ["bold", "italic"], path3);
147186
147308
  }
147187
147309
  if (options.inlineCode !== undefined) {
147188
147310
  const inlineCode2 = expectRecord2(options.inlineCode, path3, "options.inlineCode");
@@ -147199,6 +147321,23 @@ function validateOptions(value2, path3) {
147199
147321
  }
147200
147322
  }
147201
147323
  }
147324
+ function validateHeading(headings, level, allowed, path3) {
147325
+ if (headings[level] === undefined)
147326
+ return;
147327
+ const field = `options.headings["${level}"]`;
147328
+ const heading2 = expectRecord2(headings[level], path3, field);
147329
+ rejectUnknown(heading2, allowed, path3, field);
147330
+ if (heading2.startOnNewPage !== undefined) {
147331
+ expectBoolean2(heading2.startOnNewPage, path3, `${field}.startOnNewPage`);
147332
+ }
147333
+ if (heading2.alignment !== undefined) {
147334
+ expectEnum(heading2.alignment, ["left", "center"], path3, `${field}.alignment`);
147335
+ }
147336
+ if (heading2.bold !== undefined)
147337
+ expectBoolean2(heading2.bold, path3, `${field}.bold`);
147338
+ if (heading2.italic !== undefined)
147339
+ expectBoolean2(heading2.italic, path3, `${field}.italic`);
147340
+ }
147202
147341
  function rejectUnknown(value2, allowed, path3, field) {
147203
147342
  const unknown2 = Object.keys(value2).find((key) => !allowed.includes(key));
147204
147343
  if (unknown2 !== undefined)
@@ -147220,6 +147359,11 @@ function expectString2(value2, path3, field) {
147220
147359
  if (typeof value2 !== "string")
147221
147360
  invalidStyleConfig(path3, field, "必须是字符串");
147222
147361
  }
147362
+ function expectEnum(value2, allowed, path3, field) {
147363
+ if (typeof value2 !== "string" || !allowed.includes(value2)) {
147364
+ invalidStyleConfig(path3, field, `必须是 ${allowed.join("、")} 之一`);
147365
+ }
147366
+ }
147223
147367
  function invalidStyleConfig(path3, field, reason) {
147224
147368
  throw new Error(`样式配置无效:${path3}
147225
147369
  位置:${field}
@@ -147227,25 +147371,64 @@ function invalidStyleConfig(path3, field, reason) {
147227
147371
  }
147228
147372
 
147229
147373
  // src/style/compiler.ts
147230
- function loadEffectiveStyles(stylePath) {
147231
- const source = parseJson(readFileSync2(stylePath, "utf-8"), stylePath);
147232
- if (!isStyleConfig(source))
147233
- return expectStyleDefinition(source, stylePath);
147234
- const config = validateStyleConfig(source, stylePath);
147235
- const preset = expectStyleDefinition(parseJson(DEFAULT_STYLE_TEXT, "内置默认样式"), "内置默认样式");
147236
- return compileStyleConfig(preset, config);
147237
- }
147238
- function compileStyleConfig(preset, config) {
147239
- const styles = structuredClone(preset);
147374
+ var HEADING_LEVELS = ["1", "2", "3", "4", "5", "6"];
147375
+ function resolveEffectiveStyles(sources, defaults) {
147376
+ const rawPath = sources.styleRawPath ?? defaults?.styleRawPath ?? materializeDefaultStyleRaw();
147377
+ const rawStyle = loadStyleRaw(rawPath);
147378
+ if (sources.styleConfigPath) {
147379
+ return compileStyleConfig(rawStyle, loadStyleConfig(sources.styleConfigPath));
147380
+ }
147381
+ if (sources.styleRawPath)
147382
+ return rawStyle;
147383
+ const defaultConfigPath = defaults?.styleConfigPath ?? materializeDefaultStyleConfig();
147384
+ return compileStyleConfig(rawStyle, loadStyleConfig(defaultConfigPath));
147385
+ }
147386
+ function loadStyleRaw(path3) {
147387
+ const source = parseJson(readFileSync2(path3, "utf-8"), path3, "底层样式文件");
147388
+ if (!isRecord2(source))
147389
+ throw new Error(`底层样式文件必须是 JSON 对象:${path3}`);
147390
+ if (looksLikeStyleConfig(source)) {
147391
+ throw new Error(`底层样式文件无效:${path3}
147392
+ 当前文件看起来是语义化样式配置,请改用 --style-config`);
147393
+ }
147394
+ if (!looksLikeRawStyle(source)) {
147395
+ throw new Error(`底层样式文件无效:${path3}
147396
+ 缺少 Word 样式定义`);
147397
+ }
147398
+ return source;
147399
+ }
147400
+ function loadStyleConfig(path3) {
147401
+ const source = parseJson(readFileSync2(path3, "utf-8"), path3, "语义化样式配置");
147402
+ if (isRecord2(source) && looksLikeRawStyle(source)) {
147403
+ throw new Error(`语义化样式配置无效:${path3}
147404
+ 当前文件看起来是底层样式,请改用 --style-raw`);
147405
+ }
147406
+ return validateStyleConfig(source, path3);
147407
+ }
147408
+ function compileStyleConfig(rawStyle, config) {
147409
+ const styles = structuredClone(rawStyle);
147240
147410
  const options = config.options;
147241
147411
  if (!options)
147242
147412
  return styles;
147243
147413
  const firstLineIndent = options.body?.firstLineIndent;
147244
147414
  if (firstLineIndent !== undefined)
147245
147415
  applyBodyFirstLineIndent(styles, firstLineIndent);
147416
+ const lineSpacing = options.body?.lineSpacing;
147417
+ if (lineSpacing !== undefined)
147418
+ applyBodyLineSpacing(styles, lineSpacing);
147246
147419
  const startOnNewPage = options.headings?.["1"]?.startOnNewPage;
147247
147420
  if (startOnNewPage !== undefined)
147248
147421
  applyHeading1PageBreak(styles, startOnNewPage);
147422
+ const heading1Alignment = options.headings?.["1"]?.alignment;
147423
+ if (heading1Alignment !== undefined)
147424
+ applyHeading1Alignment(styles, heading1Alignment);
147425
+ for (const level of HEADING_LEVELS) {
147426
+ const heading2 = options.headings?.[level];
147427
+ if (heading2?.bold !== undefined)
147428
+ applyHeadingBold(styles, level, heading2.bold);
147429
+ if (heading2?.italic !== undefined)
147430
+ applyHeadingItalic(styles, level, heading2.italic);
147431
+ }
147249
147432
  const inlineCodeBackground = options.inlineCode?.background;
147250
147433
  if (inlineCodeBackground !== undefined) {
147251
147434
  applyInlineCodeBackground(styles, inlineCodeBackground);
@@ -147255,6 +147438,18 @@ function compileStyleConfig(preset, config) {
147255
147438
  applyCodeBlockBorder(styles, codeBlockBorder);
147256
147439
  return styles;
147257
147440
  }
147441
+ function applyBodyLineSpacing(styles, multiplier) {
147442
+ const line = Math.round(multiplier * 240);
147443
+ for (const name2 of ["First Paragraph", "Body Text"]) {
147444
+ const style = findStyle(styles, "paragraphStyles", name2);
147445
+ const paragraph2 = ensureObject(style, "paragraph");
147446
+ paragraph2.spacing = {
147447
+ ...optionalObject(paragraph2.spacing),
147448
+ line,
147449
+ lineRule: "auto"
147450
+ };
147451
+ }
147452
+ }
147258
147453
  function applyBodyFirstLineIndent(styles, enabled) {
147259
147454
  const firstParagraph = findStyle(styles, "paragraphStyles", "First Paragraph");
147260
147455
  const bodyText = findStyle(styles, "paragraphStyles", "Body Text");
@@ -147271,6 +147466,22 @@ function applyHeading1PageBreak(styles, enabled) {
147271
147466
  const heading1 = findStyle(styles, "paragraphStyles", "heading 1");
147272
147467
  ensureObject(heading1, "paragraph").pageBreakBefore = enabled;
147273
147468
  }
147469
+ function applyHeading1Alignment(styles, alignment) {
147470
+ const heading1 = findStyle(styles, "paragraphStyles", "heading 1");
147471
+ ensureObject(heading1, "paragraph").alignment = alignment;
147472
+ }
147473
+ function applyHeadingBold(styles, level, enabled) {
147474
+ const heading2 = findStyle(styles, "paragraphStyles", `heading ${level}`);
147475
+ const run = ensureObject(heading2, "run");
147476
+ run.bold = enabled;
147477
+ run.boldComplexScript = enabled;
147478
+ }
147479
+ function applyHeadingItalic(styles, level, enabled) {
147480
+ const heading2 = findStyle(styles, "paragraphStyles", `heading ${level}`);
147481
+ const run = ensureObject(heading2, "run");
147482
+ run.italics = enabled;
147483
+ run.italicsComplexScript = enabled;
147484
+ }
147274
147485
  function applyInlineCodeBackground(styles, enabled) {
147275
147486
  const inlineCode2 = findStyle(styles, "characterStyles", "Inline Code");
147276
147487
  ensureObject(inlineCode2, "run").shading = {
@@ -147297,12 +147508,12 @@ function applyCodeBlockBorder(styles, enabled) {
147297
147508
  function findStyle(styles, collectionName, name2) {
147298
147509
  const collection = styles[collectionName];
147299
147510
  if (!Array.isArray(collection)) {
147300
- throw new Error(`内置样式缺少 ${collectionName},无法应用样式配置`);
147511
+ throw new Error(`底层样式缺少 ${collectionName},无法应用语义化样式配置`);
147301
147512
  }
147302
147513
  const normalizedName = name2.toLowerCase();
147303
147514
  const style = collection.find((entry) => isRecord2(entry) && typeof entry.name === "string" && entry.name.toLowerCase() === normalizedName);
147304
147515
  if (!style)
147305
- throw new Error(`内置样式缺少“${name2}”,无法应用样式配置`);
147516
+ throw new Error(`底层样式缺少“${name2}”,无法应用语义化样式配置`);
147306
147517
  return style;
147307
147518
  }
147308
147519
  function ensureObject(parent, key) {
@@ -147316,25 +147527,30 @@ function ensureObject(parent, key) {
147316
147527
  function optionalObject(value2) {
147317
147528
  return isRecord2(value2) ? value2 : {};
147318
147529
  }
147319
- function parseJson(text5, path3) {
147530
+ function parseJson(text5, path3, kind) {
147320
147531
  try {
147321
147532
  return JSON.parse(text5);
147322
147533
  } catch (error) {
147323
147534
  const message = error instanceof Error ? error.message : String(error);
147324
- throw new Error(`样式文件不是有效的 JSON:${path3}
147535
+ throw new Error(`${kind}不是有效的 JSON:${path3}
147325
147536
  ${message}`);
147326
147537
  }
147327
147538
  }
147328
- function expectStyleDefinition(value2, path3) {
147329
- if (!isRecord2(value2))
147330
- throw new Error(`样式文件必须是 JSON 对象:${path3}`);
147331
- return value2;
147539
+ function looksLikeStyleConfig(value2) {
147540
+ return "schemaVersion" in value2 || "options" in value2 || "preset" in value2;
147541
+ }
147542
+ function looksLikeRawStyle(value2) {
147543
+ return ["default", "paragraphStyles", "characterStyles", "tableStyles", "tableStylesXml"].some((key) => (key in value2));
147332
147544
  }
147333
147545
  function isRecord2(value2) {
147334
147546
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
147335
147547
  }
147336
147548
 
147337
147549
  // src/style/generate.ts
147550
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
147551
+ import { writeFile } from "node:fs/promises";
147552
+ import { createHash as createHash2 } from "node:crypto";
147553
+ import { dirname as dirname4 } from "path";
147338
147554
  async function generateTemplateDocxFromStyles(raw, outputPath) {
147339
147555
  const { Document: Document2, Packer: Packer3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
147340
147556
  const tableStylesXml = raw.tableStylesXml;
@@ -147361,14 +147577,16 @@ async function generateTemplateDocxFromStyles(raw, outputPath) {
147361
147577
  await writeFile(outputPath, buf);
147362
147578
  }
147363
147579
  var generatePromises = new Map;
147364
- async function ensureTemplateDocx(styleJsonPath) {
147365
- const styles = loadEffectiveStyles(styleJsonPath);
147366
- const hash = createHash2("sha256").update(JSON.stringify(styles)).digest("hex").slice(0, 16);
147580
+ function styleCacheHash(rawStyle) {
147581
+ return createHash2("sha256").update(JSON.stringify(rawStyle)).digest("hex").slice(0, 16);
147582
+ }
147583
+ async function ensureTemplateDocx(rawStyle) {
147584
+ const hash = styleCacheHash(rawStyle);
147367
147585
  const outputPath = styleTemplateDocx(hash);
147368
147586
  if (!existsSync5(outputPath)) {
147369
147587
  let promise = generatePromises.get(outputPath);
147370
147588
  if (!promise) {
147371
- promise = generateTemplateDocxFromStyles(styles, outputPath).catch((error) => {
147589
+ promise = generateTemplateDocxFromStyles(rawStyle, outputPath).catch((error) => {
147372
147590
  generatePromises.delete(outputPath);
147373
147591
  throw error;
147374
147592
  });
@@ -147385,25 +147603,29 @@ async function convertMarkdown(options) {
147385
147603
  throw new Error("缺少必填参数:--file");
147386
147604
  const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
147387
147605
  const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : materializeDefaultConfig();
147388
- const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : materializeDefaultStyleConfig();
147606
+ const styleRawPath = options.styleRaw ? resolveInputPath(options.styleRaw, [".json"], "底层样式文件") : undefined;
147607
+ const styleConfigPath = options.styleConfig ? resolveInputPath(options.styleConfig, [".json"], "语义化样式配置") : undefined;
147389
147608
  const baseName = parse4(input).name;
147390
147609
  const output = resolveOutputPath(options.output, `${baseName}.docx`, [".docx"], "DOCX 输出文件");
147391
- prepareOutput(output, options.force ?? false);
147610
+ prepareOutput(output);
147392
147611
  const config = await loadConfig(configPath);
147393
147612
  const outDir = preprocessDir(input);
147394
147613
  mkdirSync5(outDir, { recursive: true });
147395
147614
  const formatted = await preprocess2(input, config, outDir);
147396
147615
  const markdownOutput = formattedMdPath(input);
147397
147616
  writeFileSync3(markdownOutput, formatted, "utf-8");
147398
- const templatePath = await ensureTemplateDocx(stylePath);
147399
- const luaFilter = materializeLuaFilter();
147617
+ const effectiveStyles = resolveEffectiveStyles({ styleRawPath, styleConfigPath });
147618
+ const templatePath = await ensureTemplateDocx(effectiveStyles);
147619
+ const inlineCodeFilter = materializeInlineCodeFilter();
147620
+ const imageSizeArgs = config.imageSize.enabled ? buildImageSizePandocArgs(config.imageSize, materializeImageSizeFilter()) : [];
147400
147621
  const pandocArgs = [
147401
147622
  markdownOutput,
147402
147623
  "-o",
147403
147624
  output,
147404
147625
  ...buildPandocResourcePathArgs(input),
147405
147626
  `--reference-doc=${templatePath}`,
147406
- ...existsSync6(luaFilter) ? [`--lua-filter=${luaFilter}`] : []
147627
+ ...existsSync6(inlineCodeFilter) ? [`--lua-filter=${inlineCodeFilter}`] : [],
147628
+ ...imageSizeArgs
147407
147629
  ];
147408
147630
  const { exitCode, stderr } = await runProcess("pandoc", pandocArgs);
147409
147631
  if (exitCode !== 0) {
@@ -147411,6 +147633,15 @@ async function convertMarkdown(options) {
147411
147633
  }
147412
147634
  console.log(`已生成:${output}`);
147413
147635
  }
147636
+ function buildImageSizePandocArgs(config, filterPath) {
147637
+ if (!config.enabled)
147638
+ return [];
147639
+ return [
147640
+ `--metadata=md2docx-image-max-width-cm:${config.maxWidthCm}`,
147641
+ `--metadata=md2docx-image-max-height-cm:${config.maxHeightCm}`,
147642
+ `--lua-filter=${filterPath}`
147643
+ ];
147644
+ }
147414
147645
  function buildPandocResourcePathArgs(inputPath, workingDirectory = process.cwd()) {
147415
147646
  const workingDir = resolve5(workingDirectory);
147416
147647
  const sourceDir = dirname5(resolve5(inputPath));
@@ -148054,23 +148285,32 @@ function extractStylesFromDocx(docxPath) {
148054
148285
  // src/commands/export.ts
148055
148286
  async function exportConfig(options) {
148056
148287
  const output = resolveOutputPath(options.output, "config.json", [".json"], "配置输出文件");
148057
- prepareOutput(output, options.force ?? false);
148288
+ prepareOutput(output);
148058
148289
  writeFileSync4(output, DEFAULT_CONFIG_TEXT, "utf-8");
148059
148290
  console.log(`已导出配置:${output}`);
148060
148291
  }
148061
- async function exportStyle(options) {
148292
+ async function exportStyleRaw(options) {
148062
148293
  const source = options.file ? resolveInputPath(options.file, [".docx"], "DOCX 文件") : undefined;
148063
- const defaultName = source ? `${parse5(source).name}_style.json` : "style.json";
148064
- const output = resolveOutputPath(options.output, defaultName, [".json"], "样式输出文件");
148065
- prepareOutput(output, options.force ?? false);
148294
+ const defaultName = defaultStyleRawOutputName(source);
148295
+ const output = resolveOutputPath(options.output, defaultName, [".json"], "底层样式输出文件");
148296
+ prepareOutput(output);
148066
148297
  if (source) {
148067
148298
  const styles = extractStylesFromDocx(source);
148068
148299
  writeFileSync4(output, `${JSON.stringify(styles, null, 2)}
148069
148300
  `, "utf-8");
148070
148301
  } else {
148071
- writeFileSync4(output, DEFAULT_STYLE_TEXT, "utf-8");
148302
+ writeFileSync4(output, DEFAULT_STYLE_RAW_TEXT, "utf-8");
148072
148303
  }
148073
- console.log(`已导出样式:${output}`);
148304
+ console.log(`已导出底层样式:${output}`);
148305
+ }
148306
+ function defaultStyleRawOutputName(source) {
148307
+ return source ? `${parse5(source).name}_style-raw.json` : "style-raw.json";
148308
+ }
148309
+ async function exportStyleConfig(options) {
148310
+ const output = resolveOutputPath(options.output, "style-config.json", [".json"], "语义化样式配置输出文件");
148311
+ prepareOutput(output);
148312
+ writeFileSync4(output, DEFAULT_STYLE_CONFIG_TEXT, "utf-8");
148313
+ console.log(`已导出语义化样式配置:${output}`);
148074
148314
  }
148075
148315
 
148076
148316
  // src/commands/format.ts
@@ -148081,17 +148321,12 @@ async function formatMarkdown(options) {
148081
148321
  const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : materializeDefaultConfig();
148082
148322
  const defaultName = `${parse6(input).name}_formatted.md`;
148083
148323
  const output = resolveOutputPath(options.output, defaultName, [".md", ".markdown"], "Markdown 输出文件");
148084
- const force = options.force ?? false;
148085
- prepareOutput(output, force);
148324
+ prepareOutput(output);
148086
148325
  const config = await loadConfig(configPath);
148087
148326
  const outputName = parse6(output).name;
148088
148327
  const assetsName = `${outputName}_assets`;
148089
148328
  const assetsDir = resolve6(dirname6(output), assetsName);
148090
148329
  if (config.renderMermaid.enabled && existsSync7(assetsDir)) {
148091
- if (!force) {
148092
- throw new Error(`资源目录已存在:${assetsDir}
148093
- 使用 --force 覆盖现有目录`);
148094
- }
148095
148330
  const parent = resolve6(dirname6(output));
148096
148331
  if (dirname6(assetsDir) !== parent)
148097
148332
  throw new Error(`拒绝清理非输出目录:${assetsDir}`);
@@ -148108,7 +148343,8 @@ async function run(args) {
148108
148343
  convert: convertMarkdown,
148109
148344
  format: formatMarkdown,
148110
148345
  exportConfig,
148111
- exportStyle,
148346
+ exportStyleRaw,
148347
+ exportStyleConfig,
148112
148348
  clean: cleanIntermediateFiles
148113
148349
  });
148114
148350
  program2.exitOverride();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v1hz/md2docx",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
5
5
  "keywords": [
6
6
  "converter",
File without changes