@v1hz/md2docx 2.1.1 → 2.3.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
@@ -17,17 +17,18 @@
17
17
 
18
18
  ## 功能
19
19
 
20
- | 功能 | 说明 |
21
- | ----------------- | ------------------------------------------------------------------ |
22
- | 文档标题 | 从 YAML frontmatter、H1 或文件名提取标题 |
23
- | 标题层级归一化 | 将最浅标题归一化为 H1,并修复标题层级跳跃 |
24
- | 标题编号 | 生成 `1`、`1.1`、`1.1.1` 等编号,并可剥离常见的已有中英文编号 |
25
- | 表格与图片编号 | 自动生成“表 1”“图 1:标题”等题注 |
26
- | Mermaid 图表 | 使用 beautiful-mermaid 和 resvg-wasm 将 Mermaid 渲染为高 DPI PNG |
27
- | Word 样式 | 根据 JSON 样式生成 Pandoc reference DOCX,也可从现有 DOCX 提取样式 |
28
- | Markdown 格式化 | 可只运行预处理流水线,输出格式化后的 Markdown |
29
- | 集中缓存 | 中间文件统一存储到 `~/.md2docx/`,不会在当前目录创建 `tmp/` |
30
- | Node 与可执行版本 | 支持 npm CLI,也支持构建不依赖 Node.js/Bun 的 Windows 可执行文件 |
20
+ | 功能 | 说明 |
21
+ | ----------------- | ---------------------------------------------------------------- |
22
+ | 文档标题 | 从 YAML frontmatter、H1 或文件名提取标题 |
23
+ | 标题层级归一化 | 将最浅标题归一化为 H1,并修复标题层级跳跃 |
24
+ | 标题编号 | 生成 `1`、`1.1`、`1.1.1` 等编号,并可剥离常见的已有中英文编号 |
25
+ | 表格与图片编号 | 自动生成“表 1”“图 1:标题”等题注 |
26
+ | Mermaid 图表 | 使用 beautiful-mermaid 和 resvg-wasm 将 Mermaid 渲染为高 DPI PNG |
27
+ | Word 样式 | 支持受控语义化配置和完整底层样式,也可从现有 DOCX 提取样式 |
28
+ | Markdown 格式化 | 可只运行预处理流水线,输出格式化后的 Markdown |
29
+ | 去除分隔符 | 默认移除 `---`、`***`、`___` 等分隔符行,减少无用页面间距 |
30
+ | 集中缓存 | 中间文件统一存储到 `~/.md2docx/`,不会在当前目录创建 `tmp/` |
31
+ | Node 与可执行版本 | 支持 npm CLI,也支持构建不依赖 Node.js/Bun 的 Windows 可执行文件 |
31
32
 
32
33
  ## 安装方式
33
34
 
@@ -96,7 +97,7 @@ md2docx clean
96
97
  | `<markdown>` | 位置参数;仅在没有其他转换选项时允许使用 |
97
98
  | `-f, --file <path>` | Markdown 输入文件 |
98
99
  | `-c, --config <path>` | 自定义配置 JSON |
99
- | `-s, --style <path>` | 自定义样式 JSON |
100
+ | `-s, --style <path>` | 语义化样式配置或完整底层样式 JSON |
100
101
  | `-o, --output <path>` | DOCX 输出路径 |
101
102
  | `--force` | 覆盖已有输出 |
102
103
  | `-h, --help` | 显示帮助 |
@@ -168,6 +169,7 @@ md2docx export style -f template.docx → ./template_style.json
168
169
  │ └── mermaid_*.png
169
170
  ├── resources/
170
171
  │ ├── config.json
172
+ │ ├── style-config.json
171
173
  │ ├── style.json
172
174
  │ └── add-inline-code.lua
173
175
  └── style/
@@ -208,10 +210,47 @@ CLI 不支持覆盖单个配置项,所有配置都通过 JSON 文件管理。
208
210
  | `renderMermaid.enabled` | 将 Mermaid 渲染为 PNG | `true` |
209
211
  | `renderMermaid.theme` | beautiful-mermaid 主题 | `"tokyo-night-light"` |
210
212
  | `renderMermaid.density` | PNG 输出 DPI,最小值 72 | `200` |
213
+ | `removeThematicBreaks.enabled` | 移除 `---`、`***`、`___` 等分隔符行 | `true` |
211
214
 
212
215
  ## 样式定制
213
216
 
214
- `config/style.json` 定义 DOCX 的默认样式、标题样式、段落样式和字符样式。转换时,项目用 `docx` 生成 reference DOCX,再通过 Pandoc 的 `--reference-doc` 应用样式。
217
+ 普通用户推荐使用受控的语义化样式配置,只修改程序明确开放的高频选项。其余颜色、尺寸、对齐方式、间距和 Word 样式继承关系继续由内置预设管理。
218
+
219
+ 仓库中的 `config/style-config.json` 是可直接复制和修改的默认配置,`config/style-config.schema.json` 用于编辑器提示和校验。未指定 `--style` 时,转换会自动加载内嵌的默认 `style-config.json`,再将它编译到内置底层样式;不需要手工传入 `-s`。
220
+
221
+ ```json
222
+ {
223
+ "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
224
+ "schemaVersion": 1,
225
+ "preset": "default",
226
+ "options": {
227
+ "body": {
228
+ "firstLineIndent": false
229
+ },
230
+ "headings": {
231
+ "1": {
232
+ "startOnNewPage": false
233
+ }
234
+ },
235
+ "inlineCode": {
236
+ "background": false
237
+ },
238
+ "codeBlock": {
239
+ "border": false
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ 将配置保存为 `style-config.json` 后使用:
246
+
247
+ ```bash
248
+ md2docx -f report.md -s style-config.json
249
+ ```
250
+
251
+ 字段缺失表示继承预设;`true` 表示使用预设效果;`false` 表示显式关闭。当前只开放正文首行缩进、一级标题另起一页、行内代码背景和代码块外框。完整设计见 [`docs/style-config-design.md`](docs/style-config-design.md)。
252
+
253
+ `config/style.json` 仍定义完整的底层 DOCX 样式,包括标题、段落、字符和表格样式。转换时,项目先将语义化配置编译到底层样式,再用 `docx` 生成 reference DOCX,最后通过 Pandoc 的 `--reference-doc` 应用样式。现有完整样式 JSON 可继续直接传给 `--style`。
215
254
 
216
255
  ### 从现有 DOCX 提取
217
256
 
@@ -220,7 +259,7 @@ md2docx export style -f template.docx
220
259
  md2docx -f report.md -s template_style.json
221
260
  ```
222
261
 
223
- ### 手动维护
262
+ ### 高级手动维护
224
263
 
225
264
  主要区域包括:
226
265
 
@@ -268,6 +307,8 @@ Markdown
268
307
 
269
308
  addTitle()
270
309
 
310
+ removeThematicBreaks()
311
+
271
312
  normalizeHeadings()
272
313
 
273
314
  numberHeadings()
@@ -287,6 +328,7 @@ Pandoc + Lua filter
287
328
  DOCX
288
329
  ```
289
330
 
331
+ `removeThematicBreaks()` 在标题处理之前执行,避免分隔符干扰文档结构。
290
332
  `numberTables()` 必须先于 Mermaid 渲染;`numberPictures()` 必须后于 Mermaid 渲染,这样 Mermaid 生成的图片也能获得图题。
291
333
 
292
334
  ## 构建
@@ -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
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
3
+ "schemaVersion": 1,
4
+ "preset": "default",
5
+ "options": {
6
+ "body": {
7
+ "firstLineIndent": true
8
+ },
9
+ "headings": {
10
+ "1": {
11
+ "startOnNewPage": false
12
+ }
13
+ },
14
+ "inlineCode": {
15
+ "background": true
16
+ },
17
+ "codeBlock": {
18
+ "border": true
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/WXY-V1hZ/md2docx/main/config/style-config.schema.json",
4
+ "title": "md2docx 语义化样式配置",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "preset"],
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "schemaVersion": {
13
+ "const": 1
14
+ },
15
+ "preset": {
16
+ "const": "default"
17
+ },
18
+ "options": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "properties": {
22
+ "body": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "firstLineIndent": {
27
+ "type": "boolean",
28
+ "description": "正文是否使用预设的首行缩进"
29
+ }
30
+ }
31
+ },
32
+ "headings": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "properties": {
36
+ "1": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "properties": {
40
+ "startOnNewPage": {
41
+ "type": "boolean",
42
+ "description": "一级标题是否另起一页"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ "inlineCode": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "background": {
53
+ "type": "boolean",
54
+ "description": "行内代码是否使用预设背景"
55
+ }
56
+ }
57
+ },
58
+ "codeBlock": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "properties": {
62
+ "border": {
63
+ "type": "boolean",
64
+ "description": "代码块是否使用预设外框"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
package/config/style.json CHANGED
@@ -559,7 +559,7 @@
559
559
  "eastAsia": "zh-CN"
560
560
  },
561
561
  "shading": {
562
- "val": "pct5",
562
+ "type": "pct5",
563
563
  "color": "auto",
564
564
  "fill": "auto"
565
565
  }
@@ -571,40 +571,40 @@
571
571
  },
572
572
  "wordWrap": false,
573
573
  "shading": {
574
- "val": "clear",
574
+ "type": "clear",
575
575
  "color": "F3F4F4",
576
576
  "fill": "FFFFFF",
577
577
  "themeFill": "background1"
578
578
  },
579
579
  "border": {
580
580
  "top": {
581
- "val": "single",
581
+ "style": "single",
582
582
  "color": "BFBFBF",
583
- "sz": 8,
583
+ "size": 8,
584
584
  "space": 10,
585
585
  "themeColor": "background1",
586
586
  "themeShade": "BF"
587
587
  },
588
588
  "left": {
589
- "val": "single",
589
+ "style": "single",
590
590
  "color": "BFBFBF",
591
- "sz": 8,
591
+ "size": 8,
592
592
  "space": 10,
593
593
  "themeColor": "background1",
594
594
  "themeShade": "BF"
595
595
  },
596
596
  "bottom": {
597
- "val": "single",
597
+ "style": "single",
598
598
  "color": "BFBFBF",
599
- "sz": 8,
599
+ "size": 8,
600
600
  "space": 10,
601
601
  "themeColor": "background1",
602
602
  "themeShade": "BF"
603
603
  },
604
604
  "right": {
605
- "val": "single",
605
+ "style": "single",
606
606
  "color": "BFBFBF",
607
- "sz": 8,
607
+ "size": 8,
608
608
  "space": 10,
609
609
  "themeColor": "background1",
610
610
  "themeShade": "BF"
@@ -750,7 +750,7 @@
750
750
  "eastAsia": "zh-CN"
751
751
  },
752
752
  "shading": {
753
- "val": "clear",
753
+ "type": "clear",
754
754
  "color": "F3F4F4",
755
755
  "fill": "FFFFFF",
756
756
  "themeFill": "background1"
@@ -844,16 +844,16 @@
844
844
  "eastAsia": "zh-CN"
845
845
  },
846
846
  "shading": {
847
- "val": "clear",
847
+ "type": "clear",
848
848
  "color": "F3F4F4",
849
849
  "fill": "F2F2F2",
850
850
  "themeFill": "background1",
851
851
  "themeFillShade": "F2"
852
852
  },
853
853
  "border": {
854
- "val": "none",
854
+ "style": "none",
855
855
  "color": "auto",
856
- "sz": 0,
856
+ "size": 0,
857
857
  "space": 0
858
858
  }
859
859
  }
package/dist/index.js CHANGED
@@ -103482,7 +103482,7 @@ Actual: ` + parser.attribValue);
103482
103482
  var options;
103483
103483
  var pureJsParser = true;
103484
103484
  var currentElement;
103485
- function validateOptions(userOptions) {
103485
+ function validateOptions2(userOptions) {
103486
103486
  options = helper.copyOptions(userOptions);
103487
103487
  helper.ensureFlagExists("ignoreDeclaration", options);
103488
103488
  helper.ensureFlagExists("ignoreInstruction", options);
@@ -103730,7 +103730,7 @@ Actual: ` + parser.attribValue);
103730
103730
  var parser = pureJsParser ? sax.parser(true, {}) : parser = new expat.Parser("UTF-8");
103731
103731
  var result = {};
103732
103732
  currentElement = result;
103733
- options = validateOptions(userOptions);
103733
+ options = validateOptions2(userOptions);
103734
103734
  if (pureJsParser) {
103735
103735
  parser.opt = { strictEntities: true };
103736
103736
  parser.onopentag = onStartElement;
@@ -103764,13 +103764,13 @@ Actual: ` + parser.attribValue);
103764
103764
  require_xml2json = /* @__PURE__ */ __commonJSMin((exports, module) => {
103765
103765
  var helper = require_options_helper();
103766
103766
  var xml2js = require_xml2js();
103767
- function validateOptions(userOptions) {
103767
+ function validateOptions2(userOptions) {
103768
103768
  var options = helper.copyOptions(userOptions);
103769
103769
  helper.ensureSpacesExists(options);
103770
103770
  return options;
103771
103771
  }
103772
103772
  module.exports = function(xml, userOptions) {
103773
- var options = validateOptions(userOptions), js = xml2js(xml, options), json, parentKey = "compact" in options && options.compact ? "_parent" : "parent";
103773
+ var options = validateOptions2(userOptions), js = xml2js(xml, options), json, parentKey = "compact" in options && options.compact ? "_parent" : "parent";
103774
103774
  if ("addParent" in options && options.addParent)
103775
103775
  json = JSON.stringify(js, function(k, v) {
103776
103776
  return k === parentKey ? "_" : v;
@@ -103784,7 +103784,7 @@ Actual: ` + parser.attribValue);
103784
103784
  var helper = require_options_helper();
103785
103785
  var isArray = require_array_helper().isArray;
103786
103786
  var currentElement, currentElementName;
103787
- function validateOptions(userOptions) {
103787
+ function validateOptions2(userOptions) {
103788
103788
  var options = helper.copyOptions(userOptions);
103789
103789
  helper.ensureFlagExists("ignoreDeclaration", options);
103790
103790
  helper.ensureFlagExists("ignoreInstruction", options);
@@ -104064,7 +104064,7 @@ Actual: ` + parser.attribValue);
104064
104064
  return xml.join("");
104065
104065
  }
104066
104066
  module.exports = function(js, options) {
104067
- options = validateOptions(options);
104067
+ options = validateOptions2(options);
104068
104068
  var xml = [];
104069
104069
  currentElement = js;
104070
104070
  currentElementName = "_root_";
@@ -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.1",
128404
+ version: "2.3.0",
128405
128405
  description: "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
128406
128406
  keywords: [
128407
128407
  "converter",
@@ -128471,7 +128471,7 @@ 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) {
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
128475
  const hasAdditionalOptions = options.config !== undefined || options.style !== undefined || options.output !== undefined || options.force !== undefined;
128476
128476
  if (markdown !== undefined) {
128477
128477
  if (options.file !== undefined || hasAdditionalOptions) {
@@ -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
  `;
@@ -146114,6 +146126,30 @@ function Code(el)
146114
146126
  end
146115
146127
  return { Code = Code }`;
146116
146128
 
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
+
146117
146153
  // config/style.json
146118
146154
  var style_default = `{
146119
146155
  "default": {
@@ -146676,7 +146712,7 @@ var style_default = `{
146676
146712
  "eastAsia": "zh-CN"
146677
146713
  },
146678
146714
  "shading": {
146679
- "val": "pct5",
146715
+ "type": "pct5",
146680
146716
  "color": "auto",
146681
146717
  "fill": "auto"
146682
146718
  }
@@ -146688,40 +146724,40 @@ var style_default = `{
146688
146724
  },
146689
146725
  "wordWrap": false,
146690
146726
  "shading": {
146691
- "val": "clear",
146727
+ "type": "clear",
146692
146728
  "color": "F3F4F4",
146693
146729
  "fill": "FFFFFF",
146694
146730
  "themeFill": "background1"
146695
146731
  },
146696
146732
  "border": {
146697
146733
  "top": {
146698
- "val": "single",
146734
+ "style": "single",
146699
146735
  "color": "BFBFBF",
146700
- "sz": 8,
146736
+ "size": 8,
146701
146737
  "space": 10,
146702
146738
  "themeColor": "background1",
146703
146739
  "themeShade": "BF"
146704
146740
  },
146705
146741
  "left": {
146706
- "val": "single",
146742
+ "style": "single",
146707
146743
  "color": "BFBFBF",
146708
- "sz": 8,
146744
+ "size": 8,
146709
146745
  "space": 10,
146710
146746
  "themeColor": "background1",
146711
146747
  "themeShade": "BF"
146712
146748
  },
146713
146749
  "bottom": {
146714
- "val": "single",
146750
+ "style": "single",
146715
146751
  "color": "BFBFBF",
146716
- "sz": 8,
146752
+ "size": 8,
146717
146753
  "space": 10,
146718
146754
  "themeColor": "background1",
146719
146755
  "themeShade": "BF"
146720
146756
  },
146721
146757
  "right": {
146722
- "val": "single",
146758
+ "style": "single",
146723
146759
  "color": "BFBFBF",
146724
- "sz": 8,
146760
+ "size": 8,
146725
146761
  "space": 10,
146726
146762
  "themeColor": "background1",
146727
146763
  "themeShade": "BF"
@@ -146867,7 +146903,7 @@ var style_default = `{
146867
146903
  "eastAsia": "zh-CN"
146868
146904
  },
146869
146905
  "shading": {
146870
- "val": "clear",
146906
+ "type": "clear",
146871
146907
  "color": "F3F4F4",
146872
146908
  "fill": "FFFFFF",
146873
146909
  "themeFill": "background1"
@@ -146961,16 +146997,16 @@ var style_default = `{
146961
146997
  "eastAsia": "zh-CN"
146962
146998
  },
146963
146999
  "shading": {
146964
- "val": "clear",
147000
+ "type": "clear",
146965
147001
  "color": "F3F4F4",
146966
147002
  "fill": "F2F2F2",
146967
147003
  "themeFill": "background1",
146968
147004
  "themeFillShade": "F2"
146969
147005
  },
146970
147006
  "border": {
146971
- "val": "none",
147007
+ "style": "none",
146972
147008
  "color": "auto",
146973
- "sz": 0,
147009
+ "size": 0,
146974
147010
  "space": 0
146975
147011
  }
146976
147012
  }
@@ -147074,6 +147110,7 @@ var style_default = `{
147074
147110
 
147075
147111
  // src/resources.ts
147076
147112
  var DEFAULT_CONFIG_TEXT = config_default;
147113
+ var DEFAULT_STYLE_CONFIG_TEXT = style_config_default;
147077
147114
  var DEFAULT_STYLE_TEXT = style_default;
147078
147115
  function materializeResource(name2, content3) {
147079
147116
  const directory = join4(TMP_DIR, "resources");
@@ -147090,18 +147127,216 @@ function materializeDefaultConfig() {
147090
147127
  function materializeDefaultStyle() {
147091
147128
  return materializeResource("style.json", DEFAULT_STYLE_TEXT);
147092
147129
  }
147130
+ function materializeDefaultStyleConfig() {
147131
+ materializeDefaultStyle();
147132
+ return materializeResource("style-config.json", DEFAULT_STYLE_CONFIG_TEXT);
147133
+ }
147093
147134
  function materializeLuaFilter() {
147094
147135
  return materializeResource("add-inline-code.lua", add_inline_code_default);
147095
147136
  }
147096
147137
 
147097
147138
  // src/style/generate.ts
147098
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "node:fs";
147139
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
147099
147140
  import { writeFile } from "node:fs/promises";
147100
147141
  import { createHash as createHash2 } from "node:crypto";
147101
147142
  import { dirname as dirname4 } from "path";
147102
- async function generateTemplateDocx(styleJsonPath, outputPath) {
147143
+
147144
+ // src/style/compiler.ts
147145
+ import { readFileSync as readFileSync2 } from "node:fs";
147146
+
147147
+ // 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
+ function validateStyleConfig(value2, path3) {
147154
+ const root2 = expectRecord2(value2, path3, "样式配置");
147155
+ rejectUnknown(root2, ["$schema", "schemaVersion", "preset", "options"], path3, "样式配置");
147156
+ if (root2.$schema !== undefined)
147157
+ expectString2(root2.$schema, path3, "$schema");
147158
+ if (root2.schemaVersion !== 1)
147159
+ invalidStyleConfig(path3, "schemaVersion", "目前只支持版本 1");
147160
+ if (root2.preset !== "default")
147161
+ invalidStyleConfig(path3, "preset", '目前只支持 "default"');
147162
+ if (root2.options !== undefined)
147163
+ validateOptions(root2.options, path3);
147164
+ return root2;
147165
+ }
147166
+ function validateOptions(value2, path3) {
147167
+ const options = expectRecord2(value2, path3, "options");
147168
+ rejectUnknown(options, ["body", "headings", "inlineCode", "codeBlock"], path3, "options");
147169
+ if (options.body !== undefined) {
147170
+ const body = expectRecord2(options.body, path3, "options.body");
147171
+ rejectUnknown(body, ["firstLineIndent"], path3, "options.body");
147172
+ if (body.firstLineIndent !== undefined) {
147173
+ expectBoolean2(body.firstLineIndent, path3, "options.body.firstLineIndent");
147174
+ }
147175
+ }
147176
+ if (options.headings !== undefined) {
147177
+ 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
+ }
147186
+ }
147187
+ if (options.inlineCode !== undefined) {
147188
+ const inlineCode2 = expectRecord2(options.inlineCode, path3, "options.inlineCode");
147189
+ rejectUnknown(inlineCode2, ["background"], path3, "options.inlineCode");
147190
+ if (inlineCode2.background !== undefined) {
147191
+ expectBoolean2(inlineCode2.background, path3, "options.inlineCode.background");
147192
+ }
147193
+ }
147194
+ if (options.codeBlock !== undefined) {
147195
+ const codeBlock = expectRecord2(options.codeBlock, path3, "options.codeBlock");
147196
+ rejectUnknown(codeBlock, ["border"], path3, "options.codeBlock");
147197
+ if (codeBlock.border !== undefined) {
147198
+ expectBoolean2(codeBlock.border, path3, "options.codeBlock.border");
147199
+ }
147200
+ }
147201
+ }
147202
+ function rejectUnknown(value2, allowed, path3, field) {
147203
+ const unknown2 = Object.keys(value2).find((key) => !allowed.includes(key));
147204
+ if (unknown2 !== undefined)
147205
+ invalidStyleConfig(path3, `${field}.${unknown2}`, "不是支持的配置项");
147206
+ }
147207
+ function expectRecord2(value2, path3, field) {
147208
+ if (!isRecord(value2))
147209
+ invalidStyleConfig(path3, field, "必须是对象");
147210
+ return value2;
147211
+ }
147212
+ function isRecord(value2) {
147213
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
147214
+ }
147215
+ function expectBoolean2(value2, path3, field) {
147216
+ if (typeof value2 !== "boolean")
147217
+ invalidStyleConfig(path3, field, "必须是 boolean");
147218
+ }
147219
+ function expectString2(value2, path3, field) {
147220
+ if (typeof value2 !== "string")
147221
+ invalidStyleConfig(path3, field, "必须是字符串");
147222
+ }
147223
+ function invalidStyleConfig(path3, field, reason) {
147224
+ throw new Error(`样式配置无效:${path3}
147225
+ 位置:${field}
147226
+ 原因:${reason}`);
147227
+ }
147228
+
147229
+ // 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);
147240
+ const options = config.options;
147241
+ if (!options)
147242
+ return styles;
147243
+ const firstLineIndent = options.body?.firstLineIndent;
147244
+ if (firstLineIndent !== undefined)
147245
+ applyBodyFirstLineIndent(styles, firstLineIndent);
147246
+ const startOnNewPage = options.headings?.["1"]?.startOnNewPage;
147247
+ if (startOnNewPage !== undefined)
147248
+ applyHeading1PageBreak(styles, startOnNewPage);
147249
+ const inlineCodeBackground = options.inlineCode?.background;
147250
+ if (inlineCodeBackground !== undefined) {
147251
+ applyInlineCodeBackground(styles, inlineCodeBackground);
147252
+ }
147253
+ const codeBlockBorder = options.codeBlock?.border;
147254
+ if (codeBlockBorder !== undefined)
147255
+ applyCodeBlockBorder(styles, codeBlockBorder);
147256
+ return styles;
147257
+ }
147258
+ function applyBodyFirstLineIndent(styles, enabled) {
147259
+ const firstParagraph = findStyle(styles, "paragraphStyles", "First Paragraph");
147260
+ const bodyText = findStyle(styles, "paragraphStyles", "Body Text");
147261
+ for (const style of [firstParagraph, bodyText]) {
147262
+ const paragraph2 = ensureObject(style, "paragraph");
147263
+ paragraph2.indent = {
147264
+ ...optionalObject(paragraph2.indent),
147265
+ firstLine: enabled ? 200 : 0,
147266
+ firstLineChars: enabled ? 200 : 0
147267
+ };
147268
+ }
147269
+ }
147270
+ function applyHeading1PageBreak(styles, enabled) {
147271
+ const heading1 = findStyle(styles, "paragraphStyles", "heading 1");
147272
+ ensureObject(heading1, "paragraph").pageBreakBefore = enabled;
147273
+ }
147274
+ function applyInlineCodeBackground(styles, enabled) {
147275
+ const inlineCode2 = findStyle(styles, "characterStyles", "Inline Code");
147276
+ ensureObject(inlineCode2, "run").shading = {
147277
+ type: "clear",
147278
+ color: enabled ? "F3F4F4" : "auto",
147279
+ fill: enabled ? "F2F2F2" : "auto"
147280
+ };
147281
+ }
147282
+ function applyCodeBlockBorder(styles, enabled) {
147283
+ const sourceCode = findStyle(styles, "paragraphStyles", "Source Code");
147284
+ const border = () => ({
147285
+ style: enabled ? "single" : "none",
147286
+ color: enabled ? "BFBFBF" : "auto",
147287
+ size: enabled ? 8 : 0,
147288
+ space: enabled ? 10 : 0
147289
+ });
147290
+ ensureObject(sourceCode, "paragraph").border = {
147291
+ top: border(),
147292
+ left: border(),
147293
+ bottom: border(),
147294
+ right: border()
147295
+ };
147296
+ }
147297
+ function findStyle(styles, collectionName, name2) {
147298
+ const collection = styles[collectionName];
147299
+ if (!Array.isArray(collection)) {
147300
+ throw new Error(`内置样式缺少 ${collectionName},无法应用样式配置`);
147301
+ }
147302
+ const normalizedName = name2.toLowerCase();
147303
+ const style = collection.find((entry) => isRecord2(entry) && typeof entry.name === "string" && entry.name.toLowerCase() === normalizedName);
147304
+ if (!style)
147305
+ throw new Error(`内置样式缺少“${name2}”,无法应用样式配置`);
147306
+ return style;
147307
+ }
147308
+ function ensureObject(parent, key) {
147309
+ const current = parent[key];
147310
+ if (isRecord2(current))
147311
+ return current;
147312
+ const created = {};
147313
+ parent[key] = created;
147314
+ return created;
147315
+ }
147316
+ function optionalObject(value2) {
147317
+ return isRecord2(value2) ? value2 : {};
147318
+ }
147319
+ function parseJson(text5, path3) {
147320
+ try {
147321
+ return JSON.parse(text5);
147322
+ } catch (error) {
147323
+ const message = error instanceof Error ? error.message : String(error);
147324
+ throw new Error(`样式文件不是有效的 JSON:${path3}
147325
+ ${message}`);
147326
+ }
147327
+ }
147328
+ function expectStyleDefinition(value2, path3) {
147329
+ if (!isRecord2(value2))
147330
+ throw new Error(`样式文件必须是 JSON 对象:${path3}`);
147331
+ return value2;
147332
+ }
147333
+ function isRecord2(value2) {
147334
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
147335
+ }
147336
+
147337
+ // src/style/generate.ts
147338
+ async function generateTemplateDocxFromStyles(raw, outputPath) {
147103
147339
  const { Document: Document2, Packer: Packer3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
147104
- const raw = JSON.parse(readFileSync2(styleJsonPath, "utf-8"));
147105
147340
  const tableStylesXml = raw.tableStylesXml;
147106
147341
  const { tableStylesXml: _, ...styles } = raw;
147107
147342
  const doc = new Document2({
@@ -147127,13 +147362,13 @@ async function generateTemplateDocx(styleJsonPath, outputPath) {
147127
147362
  }
147128
147363
  var generatePromises = new Map;
147129
147364
  async function ensureTemplateDocx(styleJsonPath) {
147130
- const raw = readFileSync2(styleJsonPath);
147131
- const hash = createHash2("sha256").update(raw).digest("hex").slice(0, 16);
147365
+ const styles = loadEffectiveStyles(styleJsonPath);
147366
+ const hash = createHash2("sha256").update(JSON.stringify(styles)).digest("hex").slice(0, 16);
147132
147367
  const outputPath = styleTemplateDocx(hash);
147133
147368
  if (!existsSync5(outputPath)) {
147134
147369
  let promise = generatePromises.get(outputPath);
147135
147370
  if (!promise) {
147136
- promise = generateTemplateDocx(styleJsonPath, outputPath).catch((error) => {
147371
+ promise = generateTemplateDocxFromStyles(styles, outputPath).catch((error) => {
147137
147372
  generatePromises.delete(outputPath);
147138
147373
  throw error;
147139
147374
  });
@@ -147150,7 +147385,7 @@ async function convertMarkdown(options) {
147150
147385
  throw new Error("缺少必填参数:--file");
147151
147386
  const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
147152
147387
  const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : materializeDefaultConfig();
147153
- const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : materializeDefaultStyle();
147388
+ const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : materializeDefaultStyleConfig();
147154
147389
  const baseName = parse4(input).name;
147155
147390
  const output = resolveOutputPath(options.output, `${baseName}.docx`, [".docx"], "DOCX 输出文件");
147156
147391
  prepareOutput(output, options.force ?? false);
@@ -147287,31 +147522,31 @@ function mapShd(shd) {
147287
147522
  if (!shd)
147288
147523
  return;
147289
147524
  const sh = {};
147290
- for (const a of [
147291
- "val",
147292
- "color",
147293
- "fill",
147294
- "themeColor",
147295
- "themeTint",
147296
- "themeFill",
147297
- "themeFillShade",
147298
- "themeShade"
147299
- ]) {
147525
+ for (const a of ["color", "fill"]) {
147300
147526
  const v = wAttr(shd, a);
147301
147527
  if (v !== null)
147302
147528
  sh[a] = v;
147303
147529
  }
147530
+ const type = wAttr(shd, "val");
147531
+ if (type !== null)
147532
+ sh.type = type;
147304
147533
  return Object.keys(sh).length > 0 ? sh : undefined;
147305
147534
  }
147306
147535
  function mapBorder(bdr) {
147307
147536
  if (!bdr)
147308
147537
  return;
147309
147538
  const b = {};
147310
- for (const a of ["val", "color", "sz", "space", "themeColor", "themeShade"]) {
147539
+ for (const a of ["color", "space"]) {
147311
147540
  const v = wAttr(bdr, a);
147312
147541
  if (v !== null)
147313
147542
  b[a] = /^\d+$/.test(v) ? Number(v) : v;
147314
147543
  }
147544
+ const style = wAttr(bdr, "val");
147545
+ if (style !== null)
147546
+ b.style = style;
147547
+ const size = wAttr(bdr, "sz");
147548
+ if (size !== null)
147549
+ b.size = /^\d+$/.test(size) ? Number(size) : size;
147315
147550
  return Object.keys(b).length > 0 ? b : undefined;
147316
147551
  }
147317
147552
  function mapRunProps(rPr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v1hz/md2docx",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
5
5
  "keywords": [
6
6
  "converter",