@v1hz/md2docx 2.2.0 → 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,18 +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
- | 去除分隔符 | 默认移除 `---`、`***`、`___` 等分隔符行,减少无用页面间距 |
30
- | 集中缓存 | 中间文件统一存储到 `~/.md2docx/`,不会在当前目录创建 `tmp/` |
31
- | 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 可执行文件 |
32
32
 
33
33
  ## 安装方式
34
34
 
@@ -97,7 +97,7 @@ md2docx clean
97
97
  | `<markdown>` | 位置参数;仅在没有其他转换选项时允许使用 |
98
98
  | `-f, --file <path>` | Markdown 输入文件 |
99
99
  | `-c, --config <path>` | 自定义配置 JSON |
100
- | `-s, --style <path>` | 自定义样式 JSON |
100
+ | `-s, --style <path>` | 语义化样式配置或完整底层样式 JSON |
101
101
  | `-o, --output <path>` | DOCX 输出路径 |
102
102
  | `--force` | 覆盖已有输出 |
103
103
  | `-h, --help` | 显示帮助 |
@@ -169,6 +169,7 @@ md2docx export style -f template.docx → ./template_style.json
169
169
  │ └── mermaid_*.png
170
170
  ├── resources/
171
171
  │ ├── config.json
172
+ │ ├── style-config.json
172
173
  │ ├── style.json
173
174
  │ └── add-inline-code.lua
174
175
  └── style/
@@ -213,7 +214,43 @@ CLI 不支持覆盖单个配置项,所有配置都通过 JSON 文件管理。
213
214
 
214
215
  ## 样式定制
215
216
 
216
- `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`。
217
254
 
218
255
  ### 从现有 DOCX 提取
219
256
 
@@ -222,7 +259,7 @@ md2docx export style -f template.docx
222
259
  md2docx -f report.md -s template_style.json
223
260
  ```
224
261
 
225
- ### 手动维护
262
+ ### 高级手动维护
226
263
 
227
264
  主要区域包括:
228
265
 
@@ -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.2.0",
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) {
@@ -146126,6 +146126,30 @@ function Code(el)
146126
146126
  end
146127
146127
  return { Code = Code }`;
146128
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
+
146129
146153
  // config/style.json
146130
146154
  var style_default = `{
146131
146155
  "default": {
@@ -146688,7 +146712,7 @@ var style_default = `{
146688
146712
  "eastAsia": "zh-CN"
146689
146713
  },
146690
146714
  "shading": {
146691
- "val": "pct5",
146715
+ "type": "pct5",
146692
146716
  "color": "auto",
146693
146717
  "fill": "auto"
146694
146718
  }
@@ -146700,40 +146724,40 @@ var style_default = `{
146700
146724
  },
146701
146725
  "wordWrap": false,
146702
146726
  "shading": {
146703
- "val": "clear",
146727
+ "type": "clear",
146704
146728
  "color": "F3F4F4",
146705
146729
  "fill": "FFFFFF",
146706
146730
  "themeFill": "background1"
146707
146731
  },
146708
146732
  "border": {
146709
146733
  "top": {
146710
- "val": "single",
146734
+ "style": "single",
146711
146735
  "color": "BFBFBF",
146712
- "sz": 8,
146736
+ "size": 8,
146713
146737
  "space": 10,
146714
146738
  "themeColor": "background1",
146715
146739
  "themeShade": "BF"
146716
146740
  },
146717
146741
  "left": {
146718
- "val": "single",
146742
+ "style": "single",
146719
146743
  "color": "BFBFBF",
146720
- "sz": 8,
146744
+ "size": 8,
146721
146745
  "space": 10,
146722
146746
  "themeColor": "background1",
146723
146747
  "themeShade": "BF"
146724
146748
  },
146725
146749
  "bottom": {
146726
- "val": "single",
146750
+ "style": "single",
146727
146751
  "color": "BFBFBF",
146728
- "sz": 8,
146752
+ "size": 8,
146729
146753
  "space": 10,
146730
146754
  "themeColor": "background1",
146731
146755
  "themeShade": "BF"
146732
146756
  },
146733
146757
  "right": {
146734
- "val": "single",
146758
+ "style": "single",
146735
146759
  "color": "BFBFBF",
146736
- "sz": 8,
146760
+ "size": 8,
146737
146761
  "space": 10,
146738
146762
  "themeColor": "background1",
146739
146763
  "themeShade": "BF"
@@ -146879,7 +146903,7 @@ var style_default = `{
146879
146903
  "eastAsia": "zh-CN"
146880
146904
  },
146881
146905
  "shading": {
146882
- "val": "clear",
146906
+ "type": "clear",
146883
146907
  "color": "F3F4F4",
146884
146908
  "fill": "FFFFFF",
146885
146909
  "themeFill": "background1"
@@ -146973,16 +146997,16 @@ var style_default = `{
146973
146997
  "eastAsia": "zh-CN"
146974
146998
  },
146975
146999
  "shading": {
146976
- "val": "clear",
147000
+ "type": "clear",
146977
147001
  "color": "F3F4F4",
146978
147002
  "fill": "F2F2F2",
146979
147003
  "themeFill": "background1",
146980
147004
  "themeFillShade": "F2"
146981
147005
  },
146982
147006
  "border": {
146983
- "val": "none",
147007
+ "style": "none",
146984
147008
  "color": "auto",
146985
- "sz": 0,
147009
+ "size": 0,
146986
147010
  "space": 0
146987
147011
  }
146988
147012
  }
@@ -147086,6 +147110,7 @@ var style_default = `{
147086
147110
 
147087
147111
  // src/resources.ts
147088
147112
  var DEFAULT_CONFIG_TEXT = config_default;
147113
+ var DEFAULT_STYLE_CONFIG_TEXT = style_config_default;
147089
147114
  var DEFAULT_STYLE_TEXT = style_default;
147090
147115
  function materializeResource(name2, content3) {
147091
147116
  const directory = join4(TMP_DIR, "resources");
@@ -147102,18 +147127,216 @@ function materializeDefaultConfig() {
147102
147127
  function materializeDefaultStyle() {
147103
147128
  return materializeResource("style.json", DEFAULT_STYLE_TEXT);
147104
147129
  }
147130
+ function materializeDefaultStyleConfig() {
147131
+ materializeDefaultStyle();
147132
+ return materializeResource("style-config.json", DEFAULT_STYLE_CONFIG_TEXT);
147133
+ }
147105
147134
  function materializeLuaFilter() {
147106
147135
  return materializeResource("add-inline-code.lua", add_inline_code_default);
147107
147136
  }
147108
147137
 
147109
147138
  // src/style/generate.ts
147110
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync2 } from "node:fs";
147139
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
147111
147140
  import { writeFile } from "node:fs/promises";
147112
147141
  import { createHash as createHash2 } from "node:crypto";
147113
147142
  import { dirname as dirname4 } from "path";
147114
- 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) {
147115
147339
  const { Document: Document2, Packer: Packer3 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
147116
- const raw = JSON.parse(readFileSync2(styleJsonPath, "utf-8"));
147117
147340
  const tableStylesXml = raw.tableStylesXml;
147118
147341
  const { tableStylesXml: _, ...styles } = raw;
147119
147342
  const doc = new Document2({
@@ -147139,13 +147362,13 @@ async function generateTemplateDocx(styleJsonPath, outputPath) {
147139
147362
  }
147140
147363
  var generatePromises = new Map;
147141
147364
  async function ensureTemplateDocx(styleJsonPath) {
147142
- const raw = readFileSync2(styleJsonPath);
147143
- 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);
147144
147367
  const outputPath = styleTemplateDocx(hash);
147145
147368
  if (!existsSync5(outputPath)) {
147146
147369
  let promise = generatePromises.get(outputPath);
147147
147370
  if (!promise) {
147148
- promise = generateTemplateDocx(styleJsonPath, outputPath).catch((error) => {
147371
+ promise = generateTemplateDocxFromStyles(styles, outputPath).catch((error) => {
147149
147372
  generatePromises.delete(outputPath);
147150
147373
  throw error;
147151
147374
  });
@@ -147162,7 +147385,7 @@ async function convertMarkdown(options) {
147162
147385
  throw new Error("缺少必填参数:--file");
147163
147386
  const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
147164
147387
  const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : materializeDefaultConfig();
147165
- const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : materializeDefaultStyle();
147388
+ const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : materializeDefaultStyleConfig();
147166
147389
  const baseName = parse4(input).name;
147167
147390
  const output = resolveOutputPath(options.output, `${baseName}.docx`, [".docx"], "DOCX 输出文件");
147168
147391
  prepareOutput(output, options.force ?? false);
@@ -147299,31 +147522,31 @@ function mapShd(shd) {
147299
147522
  if (!shd)
147300
147523
  return;
147301
147524
  const sh = {};
147302
- for (const a of [
147303
- "val",
147304
- "color",
147305
- "fill",
147306
- "themeColor",
147307
- "themeTint",
147308
- "themeFill",
147309
- "themeFillShade",
147310
- "themeShade"
147311
- ]) {
147525
+ for (const a of ["color", "fill"]) {
147312
147526
  const v = wAttr(shd, a);
147313
147527
  if (v !== null)
147314
147528
  sh[a] = v;
147315
147529
  }
147530
+ const type = wAttr(shd, "val");
147531
+ if (type !== null)
147532
+ sh.type = type;
147316
147533
  return Object.keys(sh).length > 0 ? sh : undefined;
147317
147534
  }
147318
147535
  function mapBorder(bdr) {
147319
147536
  if (!bdr)
147320
147537
  return;
147321
147538
  const b = {};
147322
- for (const a of ["val", "color", "sz", "space", "themeColor", "themeShade"]) {
147539
+ for (const a of ["color", "space"]) {
147323
147540
  const v = wAttr(bdr, a);
147324
147541
  if (v !== null)
147325
147542
  b[a] = /^\d+$/.test(v) ? Number(v) : v;
147326
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;
147327
147550
  return Object.keys(b).length > 0 ? b : undefined;
147328
147551
  }
147329
147552
  function mapRunProps(rPr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v1hz/md2docx",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "基于 pandoc 的 Markdown 转 Word 工具,自动格式化 Markdown,支持自定义样式,AI 友好,用户友好。",
5
5
  "keywords": [
6
6
  "converter",