@v1hz/md2docx 1.6.0 → 2.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1272 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/index.ts
6
+ import { CommanderError } from "commander";
7
+ import { realpathSync } from "node:fs";
8
+ import { readFile as readFile3 } from "node:fs/promises";
9
+ import { resolve as resolve5 } from "node:path";
10
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11
+
12
+ // src/cli.ts
13
+ import { Command } from "commander";
14
+ function createProgram(version, actions) {
15
+ const program = new Command;
16
+ program.name("md2docx").description("将 Markdown 转换为 Word 文档").version(version, "-v, --version", "显示版本号").helpOption("-h, --help", "显示帮助").addHelpCommand(false).showHelpAfterError("使用 --help 查看帮助").showSuggestionAfterError().enablePositionalOptions().option("-f, --file <path>", "Markdown 文件").option("-c, --config <path>", "自定义配置文件").option("-s, --style <path>", "自定义样式文件").option("-o, --output <path>", "输出 DOCX 文件").option("--force", "覆盖已有文件").action(async function(options) {
17
+ if (!options.file)
18
+ this.help();
19
+ await actions.convert(options);
20
+ });
21
+ program.exitOverride();
22
+ const exportCommand = program.command("export").description("导出默认配置或样式").helpOption("-h, --help", "显示帮助").addHelpCommand(false).action(function() {
23
+ this.help({ error: true });
24
+ });
25
+ exportCommand.exitOverride();
26
+ const exportConfigCommand = exportCommand.command("config").description("导出默认配置").helpOption("-h, --help", "显示帮助").option("-o, --output <path>", "输出 JSON 文件,默认 ./config.json").option("--force", "覆盖已有文件").action(actions.exportConfig);
27
+ exportConfigCommand.exitOverride();
28
+ 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);
29
+ exportStyleCommand.exitOverride();
30
+ const formatCommand = program.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);
31
+ formatCommand.exitOverride();
32
+ return program;
33
+ }
34
+
35
+ // src/commands/convert.ts
36
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
37
+ import { spawn } from "node:child_process";
38
+ import { parse as parse2, resolve as resolve3 } from "node:path";
39
+
40
+ // src/config.ts
41
+ import { access, readFile } from "node:fs/promises";
42
+ async function loadConfig(path) {
43
+ try {
44
+ await access(path);
45
+ } catch {
46
+ throw new Error(`找不到配置文件:${path}`);
47
+ }
48
+ let raw;
49
+ try {
50
+ raw = JSON.parse(await readFile(path, "utf-8"));
51
+ } catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ throw new Error(`配置文件不是有效的 JSON:${path}
54
+ ${message}`);
55
+ }
56
+ validateConfig(raw, path);
57
+ delete raw.$schema;
58
+ return raw;
59
+ }
60
+ function validateConfig(value, path) {
61
+ const root = expectRecord(value, path, "配置");
62
+ validateCaption(root.figureCaption, path, "figureCaption");
63
+ validateCaption(root.tableCaption, path, "tableCaption");
64
+ validateEnabled(root.normalizeHeadings, path, "normalizeHeadings");
65
+ const numbering = expectRecord(root.numberHeadings, path, "numberHeadings");
66
+ expectBoolean(numbering.enabled, path, "numberHeadings.enabled");
67
+ expectBoolean(numbering.detectExisting, path, "numberHeadings.detectExisting");
68
+ if (numbering.existingPattern !== undefined) {
69
+ expectString(numbering.existingPattern, path, "numberHeadings.existingPattern");
70
+ }
71
+ if (numbering.useBuiltinRules !== undefined) {
72
+ expectBoolean(numbering.useBuiltinRules, path, "numberHeadings.useBuiltinRules");
73
+ }
74
+ const mermaid = expectRecord(root.renderMermaid, path, "renderMermaid");
75
+ expectBoolean(mermaid.enabled, path, "renderMermaid.enabled");
76
+ expectString(mermaid.theme, path, "renderMermaid.theme");
77
+ if (!Number.isInteger(mermaid.density) || mermaid.density < 72) {
78
+ invalidConfig(path, "renderMermaid.density", "必须是不小于 72 的整数");
79
+ }
80
+ const title = expectRecord(root.detectTitle, path, "detectTitle");
81
+ expectBoolean(title.enabled, path, "detectTitle.enabled");
82
+ const strategy = expectString(title.strategy, path, "detectTitle.strategy");
83
+ if (!["first-h1", "single-h1", "filename", "none"].includes(strategy)) {
84
+ invalidConfig(path, "detectTitle.strategy", "不是支持的标题提取策略");
85
+ }
86
+ }
87
+ function validateCaption(value, path, field) {
88
+ const caption = expectRecord(value, path, field);
89
+ expectBoolean(caption.enabled, path, `${field}.enabled`);
90
+ expectString(caption.format, path, `${field}.format`);
91
+ expectString(caption.separator, path, `${field}.separator`);
92
+ }
93
+ function validateEnabled(value, path, field) {
94
+ const config = expectRecord(value, path, field);
95
+ expectBoolean(config.enabled, path, `${field}.enabled`);
96
+ }
97
+ function expectRecord(value, path, field) {
98
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
99
+ invalidConfig(path, field, "必须是对象");
100
+ }
101
+ return value;
102
+ }
103
+ function expectBoolean(value, path, field) {
104
+ if (typeof value !== "boolean")
105
+ invalidConfig(path, field, "必须是 boolean");
106
+ return value;
107
+ }
108
+ function expectString(value, path, field) {
109
+ if (typeof value !== "string")
110
+ invalidConfig(path, field, "必须是字符串");
111
+ return value;
112
+ }
113
+ function invalidConfig(path, field, reason) {
114
+ throw new Error(`配置文件无效:${path}
115
+ 位置:${field}
116
+ 原因:${reason}`);
117
+ }
118
+
119
+ // src/paths.ts
120
+ import { dirname, resolve } from "node:path";
121
+ import { fileURLToPath } from "node:url";
122
+ var PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
123
+ var CONFIG_PATH = resolve(PKG_DIR, "config/config.json");
124
+ var CONFIG_SCHEMA_PATH = resolve(PKG_DIR, "config/config.schema.json");
125
+ var TMP_DIR = "tmp";
126
+ function preprocessDir(baseName) {
127
+ return `${TMP_DIR}/preprocess/${baseName}`;
128
+ }
129
+ function formattedMdPath(baseName) {
130
+ return `${preprocessDir(baseName)}/${baseName}_formatted.md`;
131
+ }
132
+ var STYLE_DIR = `${TMP_DIR}/style`;
133
+ var STYLE_CONFIG = resolve(PKG_DIR, "config/style.json");
134
+ function styleTemplateDocx(hash) {
135
+ return `${STYLE_DIR}/${hash}.docx`;
136
+ }
137
+
138
+ // src/output.ts
139
+ import { existsSync, mkdirSync, statSync } from "node:fs";
140
+ import { dirname as dirname2, extname, resolve as resolve2 } from "node:path";
141
+ function resolveInputPath(path, extensions, label) {
142
+ const resolved = resolve2(path);
143
+ if (!existsSync(resolved))
144
+ throw new Error(`找不到${label}:${resolved}`);
145
+ if (!statSync(resolved).isFile())
146
+ throw new Error(`${label}不是文件:${resolved}`);
147
+ assertExtension(resolved, extensions, label);
148
+ return resolved;
149
+ }
150
+ function resolveOutputPath(path, defaultName, extensions, label) {
151
+ const resolved = resolve2(path ?? defaultName);
152
+ assertExtension(resolved, extensions, label);
153
+ return resolved;
154
+ }
155
+ function prepareOutput(path, force) {
156
+ if (existsSync(path) && !force) {
157
+ throw new Error(`输出文件已存在:${path}
158
+ 使用 --force 覆盖现有文件`);
159
+ }
160
+ mkdirSync(dirname2(path), { recursive: true });
161
+ }
162
+ function assertExtension(path, extensions, label) {
163
+ const extension = extname(path).toLowerCase();
164
+ if (!extensions.includes(extension)) {
165
+ throw new Error(`${label}扩展名必须是 ${extensions.join(" 或 ")}:${path}`);
166
+ }
167
+ }
168
+
169
+ // src/preprocess/index.ts
170
+ import { readFile as readFile2 } from "node:fs/promises";
171
+ import { unified } from "unified";
172
+ import remarkParse from "remark-parse";
173
+ import remarkStringify from "remark-stringify";
174
+ import remarkGfm from "remark-gfm";
175
+ import remarkFrontmatter from "remark-frontmatter";
176
+ import { visit } from "unist-util-visit";
177
+
178
+ // src/preprocess/title.ts
179
+ import { parse } from "node:path";
180
+ function addTitle(fileName, root, headings, config) {
181
+ const { enabled, strategy } = config;
182
+ if (!enabled || strategy === "none")
183
+ return;
184
+ const hasFrontmatterTitle = root.children.some((c) => c.type === "yaml" && /^title:/m.test(c.value));
185
+ if (hasFrontmatterTitle)
186
+ return;
187
+ if (strategy === "filename") {
188
+ addYamlTitle(root, fallbackTitle(fileName));
189
+ return;
190
+ }
191
+ const depth1 = headings.filter((n) => n.depth === 1);
192
+ const firstDepth1 = depth1[0];
193
+ if (strategy === "single-h1") {
194
+ if (firstDepth1 != null && depth1.length === 1) {
195
+ extractHeadingAsTitle(root, headings, firstDepth1);
196
+ return;
197
+ }
198
+ }
199
+ if (firstDepth1 != null && depth1.length === 1 && firstDepth1 === headings[0]) {
200
+ extractHeadingAsTitle(root, headings, firstDepth1);
201
+ return;
202
+ }
203
+ addYamlTitle(root, fallbackTitle(fileName));
204
+ }
205
+ function extractHeadingAsTitle(root, headings, heading) {
206
+ const title = textContent(heading);
207
+ const rootIndex = root.children.indexOf(heading);
208
+ if (rootIndex !== -1)
209
+ root.children.splice(rootIndex, 1);
210
+ const headingIndex = headings.indexOf(heading);
211
+ if (headingIndex !== -1)
212
+ headings.splice(headingIndex, 1);
213
+ addYamlTitle(root, title);
214
+ }
215
+ function textContent(node) {
216
+ if (node === null || typeof node !== "object")
217
+ return "";
218
+ const value = node;
219
+ if ((value.type === "text" || value.type === "inlineCode") && typeof value.value === "string") {
220
+ return value.value;
221
+ }
222
+ if ((value.type === "image" || value.type === "imageReference") && typeof value.alt === "string") {
223
+ return value.alt;
224
+ }
225
+ return value.children?.map(textContent).join("") ?? "";
226
+ }
227
+ function fallbackTitle(fileName) {
228
+ return parse(fileName).name;
229
+ }
230
+ function addYamlTitle(root, title) {
231
+ root.children.unshift({ type: "yaml", value: `title: ${JSON.stringify(title)}` });
232
+ }
233
+ function normalizeHeadings(nodes) {
234
+ if (nodes.length === 0)
235
+ return;
236
+ let minDepth = 6;
237
+ for (const node of nodes) {
238
+ minDepth = Math.min(minDepth, node.depth);
239
+ }
240
+ const offset = minDepth - 1;
241
+ for (let i = 0;i < nodes.length; i++) {
242
+ nodes[i].depth -= offset;
243
+ }
244
+ for (let i = 1;i < nodes.length; i++) {
245
+ if (nodes[i].depth > nodes[i - 1].depth + 1) {
246
+ nodes[i].depth = nodes[i - 1].depth + 1;
247
+ }
248
+ }
249
+ }
250
+ function numberHeadings(nodes, config) {
251
+ if (nodes.length === 0)
252
+ return;
253
+ const { detectExisting, existingPattern } = config;
254
+ const counter = [0, 0, 0, 0, 0, 0];
255
+ for (let i = 0;i <= nodes[0].depth - 2; ++i) {
256
+ counter[i] = 1;
257
+ }
258
+ for (const node of nodes) {
259
+ const d = node.depth;
260
+ counter[d - 1]++;
261
+ counter.fill(0, d);
262
+ const prefix = buildPrefix(counter);
263
+ const first = node.children[0];
264
+ if (first?.type === "text") {
265
+ const text = detectExisting ? stripHeadingNum(first.value, existingPattern, config?.useBuiltinRules) : first.value;
266
+ first.value = text ? `${prefix} ${text}` : prefix;
267
+ } else {
268
+ node.children.unshift({
269
+ type: "text",
270
+ value: prefix
271
+ });
272
+ }
273
+ }
274
+ }
275
+ function buildPrefix(counter) {
276
+ const end = counter.findLastIndex((x) => x !== 0);
277
+ return counter.slice(0, end + 1).join(".");
278
+ }
279
+ var RE_DOT_NUM = /^\d+(\.\d+)+\.?\s*/;
280
+ var RE_CN_PAREN = /^[((][一二三四五六七八九十百千]+[))]\s*/;
281
+ var RE_CN_NUM = /^[一二三四五六七八九十百千]+[、.]\s*/;
282
+ var RE_CN_DUN = /^[一二三四五六七八九十百千]+(?:、[一二三四五六七八九十百千]+)+、?\s*/;
283
+ function stripHeadingNum(text, customPattern, useBuiltinRules) {
284
+ let result = text;
285
+ if (customPattern) {
286
+ result = result.replace(new RegExp(`^${customPattern}`), "");
287
+ }
288
+ if (useBuiltinRules !== false) {
289
+ result = result.replace(new RegExp(`^(?:${[RE_CN_DUN.source, RE_CN_PAREN.source, RE_CN_NUM.source, RE_DOT_NUM.source].join("|")})`), "");
290
+ }
291
+ return result;
292
+ }
293
+
294
+ // src/preprocess/caption.ts
295
+ function numberTables(root, config) {
296
+ const {
297
+ tableCaption: { format, separator }
298
+ } = config;
299
+ let counter = 0;
300
+ const inserts = [];
301
+ const used = new Set;
302
+ for (let i = 0;i < root.children.length; i++) {
303
+ const child = root.children[i];
304
+ if (child?.type !== "table")
305
+ continue;
306
+ counter++;
307
+ const label = format.replace("{n}", String(counter));
308
+ const prevIdx = i - 1;
309
+ const prev = prevIdx >= 0 ? root.children[prevIdx] : undefined;
310
+ if (prev && isCaption(prev) && !used.has(prevIdx)) {
311
+ used.add(prevIdx);
312
+ const existing = stripTableNum(captionText(prev), format);
313
+ const first = prev.children[0];
314
+ first.value = `Table: ${label}${existing ? `${separator}${existing}` : ""}`;
315
+ continue;
316
+ }
317
+ const nextIdx = i + 1;
318
+ const next = nextIdx < root.children.length ? root.children[nextIdx] : undefined;
319
+ if (next && isCaption(next) && !used.has(nextIdx)) {
320
+ used.add(nextIdx);
321
+ const existing = stripTableNum(captionText(next), format);
322
+ const first = next.children[0];
323
+ first.value = `Table: ${label}${existing ? `${separator}${existing}` : ""}`;
324
+ continue;
325
+ }
326
+ inserts.push({ at: i + 1, text: `Table: ${label}` });
327
+ }
328
+ for (let j = inserts.length - 1;j >= 0; j--) {
329
+ const { at, text } = inserts[j];
330
+ root.children.splice(at, 0, {
331
+ type: "paragraph",
332
+ children: [{ type: "text", value: text }]
333
+ });
334
+ }
335
+ }
336
+ function numberPictures(root, config) {
337
+ const {
338
+ figureCaption: { format, separator }
339
+ } = config;
340
+ let counter = 0;
341
+ for (let i = 0;i < root.children.length; i++) {
342
+ const child = root.children[i];
343
+ if (child?.type === "paragraph" && child.children.length === 1 && child.children[0]?.type === "image") {
344
+ counter++;
345
+ const img = child.children[0];
346
+ const cleaned = stripPictureNum(img.title ?? img.alt ?? "", format);
347
+ const label = cleaned || fileNameFromUrl(img.url) || "";
348
+ const prefix = format.replace("{n}", String(counter));
349
+ img.alt = `${prefix}${label ? `${separator}${label}` : ""}`;
350
+ }
351
+ }
352
+ }
353
+ function fileNameFromUrl(url) {
354
+ const match = url.match(/\/([^/]+?)(?:\.[^/.]+)?$/);
355
+ return match ? match[1] : null;
356
+ }
357
+ function isCaption(node) {
358
+ return node != null && typeof node === "object" && node.type === "paragraph" && node.children?.length === 1 && node.children[0]?.type === "text" && (node.children[0]?.value ?? "").startsWith("Table: ");
359
+ }
360
+ function captionText(node) {
361
+ const first = node.children[0];
362
+ if (first?.type === "text") {
363
+ return first.value.replace(/^Table: /, "");
364
+ }
365
+ return "";
366
+ }
367
+ function stripTableNum(text, format) {
368
+ const escaped = format.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&").replace("{n}", "\\d+");
369
+ return text.replace(new RegExp(`^${escaped}`), "");
370
+ }
371
+ function stripPictureNum(text, format) {
372
+ const escaped = format.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&").replace("{n}", "\\d+");
373
+ return text.replace(new RegExp(`^${escaped}`), "");
374
+ }
375
+
376
+ // src/preprocess/mermaid.ts
377
+ import { writeFileSync, mkdirSync as mkdirSync2 } from "fs";
378
+ import { join } from "path";
379
+ import { renderMermaidSVG, THEMES } from "beautiful-mermaid";
380
+ import sharp from "sharp";
381
+ async function renderMermaid(root, outDir, theme, density, assetUrlDir = outDir) {
382
+ const mermaidTheme = THEMES[theme] ?? THEMES["tokyo-night-light"];
383
+ let counter = 0;
384
+ for (let i = 0;i < root.children.length; i++) {
385
+ const child = root.children[i];
386
+ if (child?.type !== "code" || child.lang !== "mermaid")
387
+ continue;
388
+ counter++;
389
+ const fileName = `mermaid_${counter}.png`;
390
+ const pngFile = join(outDir, fileName);
391
+ const title = getMermaidTitle(child.meta);
392
+ try {
393
+ let svg = renderMermaidSVG(child.value, mermaidTheme);
394
+ svg = resolveCSSVars(svg);
395
+ const png = await sharp(Buffer.from(svg), {
396
+ density
397
+ }).png().toBuffer();
398
+ mkdirSync2(outDir, { recursive: true });
399
+ writeFileSync(pngFile, png);
400
+ } catch (err) {
401
+ console.error(`mermaid 渲染失败 (#${counter}):`, err);
402
+ continue;
403
+ }
404
+ root.children[i] = {
405
+ type: "paragraph",
406
+ children: [
407
+ {
408
+ type: "image",
409
+ url: join(assetUrlDir, fileName).replaceAll("\\", "/"),
410
+ title: title ?? undefined,
411
+ alt: title ?? ""
412
+ }
413
+ ]
414
+ };
415
+ }
416
+ }
417
+ function getMermaidTitle(meta) {
418
+ if (!meta)
419
+ return null;
420
+ const match = meta.match(/title\s*=\s*["']([^"']+)["']/);
421
+ return match ? match[1] : null;
422
+ }
423
+ function resolveCSSVars(svg) {
424
+ const vars = {};
425
+ const styleAttr = svg.match(/<svg[^>]*style="([^"]+)"/)?.[1];
426
+ if (!styleAttr)
427
+ return svg;
428
+ styleAttr.replace(/--([\w-]+)\s*:\s*([^;]+)/g, (_, n, v) => {
429
+ vars[n] = v.trim();
430
+ return "";
431
+ });
432
+ const styleBlock = svg.match(/<style>([\s\S]*?)<\/style>/)?.[1];
433
+ if (styleBlock) {
434
+ const svgRules = styleBlock.match(/svg\s*\{([^}]+)\}/)?.[1];
435
+ if (svgRules) {
436
+ svgRules.replace(/--([\w-]+)\s*:\s*([^;]+)/g, (_, n, v) => {
437
+ vars[n] = v.trim();
438
+ return "";
439
+ });
440
+ }
441
+ }
442
+ let changed = true;
443
+ while (changed) {
444
+ changed = false;
445
+ for (const key of Object.keys(vars)) {
446
+ const newVal = vars[key].replace(/var\(--([\w-]+)\)/g, (_, name) => {
447
+ const resolved = vars[name];
448
+ if (resolved) {
449
+ changed = true;
450
+ return resolved;
451
+ }
452
+ return `var(--${name})`;
453
+ });
454
+ vars[key] = newVal;
455
+ }
456
+ }
457
+ function resolveColorMix(expr) {
458
+ const colors = getMixColors(expr);
459
+ if (!colors)
460
+ return expr;
461
+ return blendColors(colors.c1, colors.c2, colors.pct);
462
+ }
463
+ for (const key of Object.keys(vars)) {
464
+ vars[key] = vars[key].replace(/color-mix\([^)]+\)/g, (m) => resolveColorMix(m));
465
+ }
466
+ return svg.replace(/var\(--([\w-]+)\)/g, (_, name) => {
467
+ return vars[name] ?? `var(--${name})`;
468
+ });
469
+ }
470
+ function getMixColors(expr) {
471
+ const match = expr.match(/color-mix\(\s*in\s+srgb\s*,\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})\s+(\d+(?:\.\d+)?)%\s*,\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})\s*\)/);
472
+ if (!match)
473
+ return null;
474
+ return { c1: match[1], c2: match[3], pct: parseFloat(match[2]) / 100 };
475
+ }
476
+ function blendColors(c1, c2, pct) {
477
+ const r1 = Number.parseInt(c1.slice(1, 3), 16);
478
+ const g1 = Number.parseInt(c1.slice(3, 5), 16);
479
+ const b1 = Number.parseInt(c1.slice(5, 7), 16);
480
+ const r2 = Number.parseInt(c2.slice(1, 3), 16);
481
+ const g2 = Number.parseInt(c2.slice(3, 5), 16);
482
+ const b2 = Number.parseInt(c2.slice(5, 7), 16);
483
+ const r = Math.round(r1 * pct + r2 * (1 - pct));
484
+ const g = Math.round(g1 * pct + g2 * (1 - pct));
485
+ const b = Math.round(b1 * pct + b2 * (1 - pct));
486
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
487
+ }
488
+
489
+ // src/preprocess/index.ts
490
+ async function preprocess(mdPath, cfg, outDir, assetUrlDir = outDir) {
491
+ const md = await readFile2(mdPath, "utf-8");
492
+ const processor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkStringify, { resourceLink: true, bullet: "-" });
493
+ const ast = processor.parse(md);
494
+ const headings = [];
495
+ visit(ast, "heading", (heading) => {
496
+ headings.push(heading);
497
+ });
498
+ addTitle(mdPath, ast, headings, cfg.detectTitle);
499
+ if (cfg.normalizeHeadings.enabled || cfg.numberHeadings.enabled) {
500
+ normalizeHeadings(headings);
501
+ }
502
+ if (cfg.numberHeadings.enabled) {
503
+ numberHeadings(headings, cfg.numberHeadings);
504
+ }
505
+ if (cfg.tableCaption.enabled) {
506
+ numberTables(ast, cfg);
507
+ }
508
+ if (cfg.renderMermaid.enabled) {
509
+ await renderMermaid(ast, outDir, cfg.renderMermaid.theme, cfg.renderMermaid.density, assetUrlDir);
510
+ }
511
+ if (cfg.figureCaption.enabled) {
512
+ numberPictures(ast, cfg);
513
+ }
514
+ return processor.stringify(ast);
515
+ }
516
+
517
+ // src/style/generate.ts
518
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync } from "node:fs";
519
+ import { writeFile } from "node:fs/promises";
520
+ import { createHash } from "node:crypto";
521
+ import { Document, Packer } from "docx";
522
+ import { dirname as dirname3 } from "path";
523
+ async function generateTemplateDocx(styleJsonPath, outputPath) {
524
+ const raw = JSON.parse(readFileSync(styleJsonPath, "utf-8"));
525
+ const tableStylesXml = raw.tableStylesXml;
526
+ const { tableStylesXml: _, ...styles } = raw;
527
+ const doc = new Document({
528
+ styles,
529
+ sections: []
530
+ });
531
+ const buf = await Packer.toBuffer(doc);
532
+ mkdirSync3(dirname3(outputPath), { recursive: true });
533
+ if (tableStylesXml) {
534
+ const PizZip = (await import("pizzip")).default;
535
+ const zip = new PizZip(buf);
536
+ const stylesFile = zip.file("word/styles.xml");
537
+ if (stylesFile) {
538
+ const originalXml = stylesFile.asText();
539
+ const injectedXml = originalXml.replace("</w:styles>", tableStylesXml + "</w:styles>");
540
+ zip.file("word/styles.xml", injectedXml);
541
+ const modifiedBuf = zip.generate({ type: "nodebuffer" });
542
+ await writeFile(outputPath, modifiedBuf);
543
+ return;
544
+ }
545
+ }
546
+ await writeFile(outputPath, buf);
547
+ }
548
+ var generatePromises = new Map;
549
+ async function ensureTemplateDocx(styleJsonPath) {
550
+ const raw = readFileSync(styleJsonPath);
551
+ const hash = createHash("sha256").update(raw).digest("hex").slice(0, 16);
552
+ const outputPath = styleTemplateDocx(hash);
553
+ if (!existsSync2(outputPath)) {
554
+ let promise = generatePromises.get(outputPath);
555
+ if (!promise) {
556
+ promise = generateTemplateDocx(styleJsonPath, outputPath).catch((error) => {
557
+ generatePromises.delete(outputPath);
558
+ throw error;
559
+ });
560
+ generatePromises.set(outputPath, promise);
561
+ }
562
+ await promise;
563
+ }
564
+ return outputPath;
565
+ }
566
+
567
+ // src/commands/convert.ts
568
+ async function convertMarkdown(options) {
569
+ if (!options.file)
570
+ throw new Error("缺少必填参数:--file");
571
+ const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
572
+ const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : CONFIG_PATH;
573
+ const stylePath = options.style ? resolveInputPath(options.style, [".json"], "样式文件") : STYLE_CONFIG;
574
+ const baseName = parse2(input).name;
575
+ const output = resolveOutputPath(options.output, `${baseName}.docx`, [".docx"], "DOCX 输出文件");
576
+ prepareOutput(output, options.force ?? false);
577
+ const config = await loadConfig(configPath);
578
+ const outDir = resolve3(preprocessDir(baseName));
579
+ mkdirSync4(outDir, { recursive: true });
580
+ const formatted = await preprocess(input, config, outDir);
581
+ const markdownOutput = resolve3(formattedMdPath(baseName));
582
+ writeFileSync2(markdownOutput, formatted, "utf-8");
583
+ const templatePath = await ensureTemplateDocx(stylePath);
584
+ const luaFilter = resolve3(PKG_DIR, "config/lua/add-inline-code.lua");
585
+ const pandocArgs = [
586
+ markdownOutput,
587
+ "-o",
588
+ output,
589
+ `--reference-doc=${templatePath}`,
590
+ ...existsSync3(luaFilter) ? [`--lua-filter=${luaFilter}`] : []
591
+ ];
592
+ const { exitCode, stderr } = await runProcess("pandoc", pandocArgs);
593
+ if (exitCode !== 0) {
594
+ throw new Error(`pandoc 转换失败 (exit code ${exitCode}):${stderr.trim()}`);
595
+ }
596
+ console.log(`已生成:${output}`);
597
+ }
598
+ function runProcess(command, args) {
599
+ return new Promise((resolve4, reject) => {
600
+ const child = spawn(command, args, { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
601
+ let stderr = "";
602
+ child.stderr.setEncoding("utf-8");
603
+ child.stderr.on("data", (chunk) => {
604
+ stderr += chunk;
605
+ });
606
+ child.once("error", reject);
607
+ child.once("close", (exitCode) => resolve4({ exitCode: exitCode ?? 1, stderr }));
608
+ });
609
+ }
610
+
611
+ // src/commands/export.ts
612
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
613
+ import { copyFile } from "node:fs/promises";
614
+ import { parse as parse3 } from "node:path";
615
+
616
+ // src/style/extract.ts
617
+ import { readFileSync as readFileSync2 } from "node:fs";
618
+ import { DOMParser } from "@xmldom/xmldom";
619
+ import { useNamespaces } from "xpath";
620
+ import PizZip from "pizzip";
621
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
622
+ var x = useNamespaces({ w: W });
623
+ function one(node, expr) {
624
+ const result = x(expr, node);
625
+ if (!result)
626
+ return null;
627
+ if (Array.isArray(result))
628
+ return result[0] ?? null;
629
+ return result;
630
+ }
631
+ function all(node, expr) {
632
+ const result = x(expr, node);
633
+ if (!result)
634
+ return [];
635
+ if (Array.isArray(result))
636
+ return result;
637
+ return [result];
638
+ }
639
+ function wAttr(el, local) {
640
+ if (!el)
641
+ return null;
642
+ return el.getAttributeNS(W, local) ?? null;
643
+ }
644
+ function wNumber(el, local = "val") {
645
+ const value = wAttr(el, local);
646
+ if (value === null || value.trim() === "")
647
+ return null;
648
+ const number = Number(value);
649
+ return Number.isFinite(number) ? number : null;
650
+ }
651
+ function wBool(el, local) {
652
+ if (!el)
653
+ return null;
654
+ const found = one(el, `w:${local}`);
655
+ if (!found)
656
+ return null;
657
+ const v = wAttr(found, "val");
658
+ return v === null || !["0", "false", "off", "no"].includes(v.toLowerCase());
659
+ }
660
+ function mapShd(shd) {
661
+ if (!shd)
662
+ return;
663
+ const sh = {};
664
+ for (const a of [
665
+ "val",
666
+ "color",
667
+ "fill",
668
+ "themeColor",
669
+ "themeTint",
670
+ "themeFill",
671
+ "themeFillShade",
672
+ "themeShade"
673
+ ]) {
674
+ const v = wAttr(shd, a);
675
+ if (v !== null)
676
+ sh[a] = v;
677
+ }
678
+ return Object.keys(sh).length > 0 ? sh : undefined;
679
+ }
680
+ function mapBorder(bdr) {
681
+ if (!bdr)
682
+ return;
683
+ const b = {};
684
+ for (const a of ["val", "color", "sz", "space", "themeColor", "themeShade"]) {
685
+ const v = wAttr(bdr, a);
686
+ if (v !== null)
687
+ b[a] = /^\d+$/.test(v) ? Number(v) : v;
688
+ }
689
+ return Object.keys(b).length > 0 ? b : undefined;
690
+ }
691
+ function mapRunProps(rPr) {
692
+ if (!rPr)
693
+ return {};
694
+ const out = {};
695
+ const rFonts = one(rPr, "w:rFonts");
696
+ if (rFonts) {
697
+ const font = {};
698
+ for (const a of [
699
+ "ascii",
700
+ "hAnsi",
701
+ "cs",
702
+ "eastAsia",
703
+ "asciiTheme",
704
+ "hAnsiTheme",
705
+ "eastAsiaTheme",
706
+ "cstheme"
707
+ ]) {
708
+ const v = wAttr(rFonts, a);
709
+ if (v)
710
+ font[a] = v;
711
+ }
712
+ if (Object.keys(font).length)
713
+ out.font = font;
714
+ }
715
+ const sz = one(rPr, "w:sz");
716
+ if (sz) {
717
+ const v = wNumber(sz);
718
+ if (v !== null)
719
+ out.size = v;
720
+ }
721
+ const szCs = one(rPr, "w:szCs");
722
+ if (szCs) {
723
+ const v = wNumber(szCs);
724
+ if (v !== null)
725
+ out.sizeComplexScript = v;
726
+ }
727
+ const color = one(rPr, "w:color");
728
+ if (color) {
729
+ const v = wAttr(color, "val");
730
+ if (v)
731
+ out.color = v.toUpperCase();
732
+ }
733
+ const boolMap = [
734
+ ["b", "bold"],
735
+ ["bCs", "boldComplexScript"],
736
+ ["i", "italics"],
737
+ ["iCs", "italicsComplexScript"],
738
+ ["strike", "strike"],
739
+ ["dstrike", "doubleStrike"],
740
+ ["smallCaps", "smallCaps"],
741
+ ["caps", "allCaps"],
742
+ ["vanish", "vanish"],
743
+ ["emboss", "emboss"],
744
+ ["imprint", "imprint"]
745
+ ];
746
+ for (const [tag, key] of boolMap) {
747
+ const v = wBool(rPr, tag);
748
+ if (v !== null)
749
+ out[key] = v;
750
+ }
751
+ const va = one(rPr, "w:vertAlign");
752
+ if (va) {
753
+ const v = wAttr(va, "val");
754
+ if (v === "superscript")
755
+ out.superScript = true;
756
+ else if (v === "subscript")
757
+ out.subScript = true;
758
+ }
759
+ const u = one(rPr, "w:u");
760
+ if (u) {
761
+ const ul = {};
762
+ const ut = wAttr(u, "val");
763
+ if (ut)
764
+ ul.type = ut;
765
+ const uc = wAttr(u, "color");
766
+ if (uc)
767
+ ul.color = uc;
768
+ out.underline = ul;
769
+ }
770
+ const sp = one(rPr, "w:spacing");
771
+ if (sp) {
772
+ const v = wNumber(sp);
773
+ if (v !== null)
774
+ out.characterSpacing = v;
775
+ }
776
+ const lang = one(rPr, "w:lang");
777
+ if (lang) {
778
+ const lo = {};
779
+ for (const a of ["val", "eastAsia", "bidi"]) {
780
+ const v = wAttr(lang, a);
781
+ if (v)
782
+ lo[a] = v;
783
+ }
784
+ if (Object.keys(lo).length)
785
+ out.language = lo;
786
+ }
787
+ const shd = mapShd(one(rPr, "w:shd"));
788
+ if (shd)
789
+ out.shading = shd;
790
+ const bdr = one(rPr, "w:bdr");
791
+ if (bdr) {
792
+ const b = mapBorder(bdr);
793
+ if (b)
794
+ out.border = b;
795
+ }
796
+ if (one(rPr, "w:noProof"))
797
+ out.noProof = true;
798
+ return out;
799
+ }
800
+ function mapParagraphProps(pPr) {
801
+ if (!pPr)
802
+ return {};
803
+ const out = {};
804
+ const ALIGN_MAP = {
805
+ left: "left",
806
+ center: "center",
807
+ right: "right",
808
+ both: "both",
809
+ start: "start",
810
+ end: "end"
811
+ };
812
+ const jc = one(pPr, "w:jc");
813
+ if (jc) {
814
+ const v = wAttr(jc, "val");
815
+ if (v && v in ALIGN_MAP)
816
+ out.alignment = ALIGN_MAP[v];
817
+ }
818
+ const spacing = one(pPr, "w:spacing");
819
+ if (spacing) {
820
+ const sp = {};
821
+ for (const [tag, key] of [
822
+ ["before", "before"],
823
+ ["after", "after"],
824
+ ["line", "line"]
825
+ ]) {
826
+ const v = wNumber(spacing, tag);
827
+ if (v !== null)
828
+ sp[key] = v;
829
+ }
830
+ const lr = wAttr(spacing, "lineRule");
831
+ if (lr !== null)
832
+ sp.lineRule = lr;
833
+ for (const tag of ["beforeLines", "afterLines", "beforeAutospacing", "afterAutospacing"]) {
834
+ const v = wAttr(spacing, tag);
835
+ if (v !== null)
836
+ sp[tag] = v;
837
+ }
838
+ if (Object.keys(sp).length)
839
+ out.spacing = sp;
840
+ }
841
+ const ind = one(pPr, "w:ind");
842
+ if (ind) {
843
+ const id = {};
844
+ for (const a of [
845
+ "left",
846
+ "right",
847
+ "firstLine",
848
+ "hanging",
849
+ "start",
850
+ "end",
851
+ "leftChars",
852
+ "rightChars",
853
+ "firstLineChars",
854
+ "hangingChars"
855
+ ]) {
856
+ const v = wNumber(ind, a);
857
+ if (v !== null)
858
+ id[a] = v;
859
+ }
860
+ if (Object.keys(id).length)
861
+ out.indent = id;
862
+ }
863
+ for (const tag of [
864
+ "keepNext",
865
+ "keepLines",
866
+ "pageBreakBefore",
867
+ "widowControl",
868
+ "contextualSpacing",
869
+ "wordWrap",
870
+ "suppressLineNumbers",
871
+ "suppressAutoHyphens"
872
+ ]) {
873
+ const v = wBool(pPr, tag);
874
+ if (v !== null)
875
+ out[tag] = v;
876
+ }
877
+ const ol = one(pPr, "w:outlineLvl");
878
+ if (ol) {
879
+ const v = wNumber(ol);
880
+ if (v !== null)
881
+ out.outlineLevel = v;
882
+ }
883
+ const shd = mapShd(one(pPr, "w:shd"));
884
+ if (shd)
885
+ out.shading = shd;
886
+ const pBdr = one(pPr, "w:pBdr");
887
+ if (pBdr) {
888
+ const borders = {};
889
+ for (const side of ["top", "left", "bottom", "right"]) {
890
+ const b = mapBorder(one(pBdr, `w:${side}`));
891
+ if (b)
892
+ borders[side] = b;
893
+ }
894
+ if (Object.keys(borders).length)
895
+ out.border = borders;
896
+ }
897
+ const tabs = one(pPr, "w:tabs");
898
+ if (tabs) {
899
+ const tabList = [];
900
+ for (const tab of all(tabs, "w:tab")) {
901
+ const t = {};
902
+ const v = wAttr(tab, "val");
903
+ const pos = wNumber(tab, "pos");
904
+ if (v)
905
+ t.val = v;
906
+ if (pos !== null)
907
+ t.pos = pos;
908
+ if (Object.keys(t).length)
909
+ tabList.push(t);
910
+ }
911
+ if (tabList.length)
912
+ out.tabs = tabList;
913
+ }
914
+ return out;
915
+ }
916
+ function mapTableProps(tblPr) {
917
+ if (!tblPr)
918
+ return;
919
+ const out = {};
920
+ const jc = one(tblPr, "w:jc");
921
+ if (jc) {
922
+ const v = wAttr(jc, "val");
923
+ if (v)
924
+ out.alignment = v;
925
+ }
926
+ const tblInd = one(tblPr, "w:tblInd");
927
+ if (tblInd) {
928
+ const id = {};
929
+ const w = wNumber(tblInd, "w");
930
+ if (w !== null)
931
+ id.width = w;
932
+ const t = wAttr(tblInd, "type");
933
+ if (t !== null)
934
+ id.type = t;
935
+ if (Object.keys(id).length)
936
+ out.indent = id;
937
+ }
938
+ const tblBorders = one(tblPr, "w:tblBorders");
939
+ if (tblBorders) {
940
+ const borders = {};
941
+ for (const side of ["top", "left", "bottom", "right", "insideH", "insideV"]) {
942
+ const b = mapBorder(one(tblBorders, `w:${side}`));
943
+ if (b)
944
+ borders[side] = b;
945
+ }
946
+ if (Object.keys(borders).length)
947
+ out.borders = borders;
948
+ }
949
+ const tblCellMar = one(tblPr, "w:tblCellMar");
950
+ if (tblCellMar) {
951
+ const margins = {};
952
+ for (const side of ["top", "left", "bottom", "right"]) {
953
+ const v = wNumber(one(tblCellMar, `w:${side}`), "w");
954
+ if (v !== null)
955
+ margins[side] = v;
956
+ }
957
+ if (Object.keys(margins).length)
958
+ out.cellMargin = margins;
959
+ }
960
+ return Object.keys(out).length > 0 ? out : undefined;
961
+ }
962
+ function mapTableStylePr(tblStylePr) {
963
+ if (!tblStylePr)
964
+ return;
965
+ const type = wAttr(tblStylePr, "type");
966
+ const out = {};
967
+ if (type)
968
+ out.type = type;
969
+ const rPr = mapRunProps(one(tblStylePr, "w:rPr"));
970
+ if (Object.keys(rPr).length)
971
+ out.run = rPr;
972
+ const pPr = mapParagraphProps(one(tblStylePr, "w:pPr"));
973
+ if (Object.keys(pPr).length)
974
+ out.paragraph = pPr;
975
+ const tblPr = one(tblStylePr, "w:tblPr");
976
+ if (tblPr) {
977
+ const tbl = mapTableProps(tblPr);
978
+ if (tbl)
979
+ out.table = tbl;
980
+ }
981
+ const tcPr = one(tblStylePr, "w:tcPr");
982
+ if (tcPr) {
983
+ const tc = {};
984
+ const tcBorders = one(tcPr, "w:tcBorders");
985
+ if (tcBorders) {
986
+ const borders = {};
987
+ for (const side of ["top", "left", "bottom", "right", "insideH", "insideV"]) {
988
+ const b = mapBorder(one(tcBorders, `w:${side}`));
989
+ if (b)
990
+ borders[side] = b;
991
+ }
992
+ if (Object.keys(borders).length)
993
+ tc.borders = borders;
994
+ }
995
+ const vAlign = one(tcPr, "w:vAlign");
996
+ if (vAlign) {
997
+ const v = wAttr(vAlign, "val");
998
+ if (v)
999
+ tc.verticalAlign = v;
1000
+ }
1001
+ if (Object.keys(tc).length)
1002
+ out.cell = tc;
1003
+ }
1004
+ return Object.keys(out).length > 0 ? out : undefined;
1005
+ }
1006
+ function serializeElement(el) {
1007
+ let result = "<" + el.tagName;
1008
+ for (let i = 0;i < el.attributes.length; i++) {
1009
+ const attr = el.attributes[i];
1010
+ result += ` ${attr.name}="${attr.value.replace(/"/g, "&quot;")}"`;
1011
+ }
1012
+ if (el.childNodes.length === 0) {
1013
+ result += "/>";
1014
+ } else {
1015
+ result += ">";
1016
+ for (let i = 0;i < el.childNodes.length; i++) {
1017
+ const child = el.childNodes[i];
1018
+ if (child.nodeType === 1) {
1019
+ result += serializeElement(child);
1020
+ } else if (child.nodeType === 3) {
1021
+ result += child.data;
1022
+ }
1023
+ }
1024
+ result += "</" + el.tagName + ">";
1025
+ }
1026
+ return result;
1027
+ }
1028
+ var BUILTIN_PARA = {
1029
+ Title: "title",
1030
+ Heading1: "heading1",
1031
+ Heading2: "heading2",
1032
+ Heading3: "heading3",
1033
+ Heading4: "heading4",
1034
+ Heading5: "heading5",
1035
+ Heading6: "heading6",
1036
+ Strong: "strong",
1037
+ ListParagraph: "listParagraph",
1038
+ FootnoteText: "footnoteText",
1039
+ EndnoteText: "endnoteText"
1040
+ };
1041
+ var BUILTIN_CHAR = {
1042
+ Hyperlink: "hyperlink",
1043
+ FootnoteReference: "footnoteReference",
1044
+ FootnoteTextChar: "footnoteTextChar",
1045
+ EndnoteReference: "endnoteReference",
1046
+ EndnoteTextChar: "endnoteTextChar"
1047
+ };
1048
+ function extractStylesFromDocx(docxPath) {
1049
+ const zip = new PizZip(readFileSync2(docxPath));
1050
+ const stylesFile = zip.file("word/styles.xml");
1051
+ if (!stylesFile)
1052
+ throw new Error("word/styles.xml not found in docx");
1053
+ const xml = stylesFile.asText();
1054
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
1055
+ const parseError = doc.getElementsByTagName("parsererror").item(0);
1056
+ if (parseError)
1057
+ throw new Error(`Invalid word/styles.xml: ${parseError.textContent}`);
1058
+ const defaultDoc = {};
1059
+ const defaultOverrides = {};
1060
+ const paragraphStyles = [];
1061
+ const characterStyles = [];
1062
+ const tableStyles = [];
1063
+ const xmlDocument = doc;
1064
+ const dd = one(xmlDocument, "//w:docDefaults");
1065
+ if (dd) {
1066
+ const rPrDef = one(dd, "w:rPrDefault/w:rPr");
1067
+ if (rPrDef) {
1068
+ const run = mapRunProps(rPrDef);
1069
+ if (Object.keys(run).length)
1070
+ defaultDoc.run = run;
1071
+ }
1072
+ const pPrDef = one(dd, "w:pPrDefault/w:pPr");
1073
+ if (pPrDef) {
1074
+ const para = mapParagraphProps(pPrDef);
1075
+ if (Object.keys(para).length)
1076
+ defaultDoc.paragraph = para;
1077
+ }
1078
+ }
1079
+ for (const style of all(xmlDocument, "//w:style")) {
1080
+ const styleId = wAttr(style, "styleId") ?? "";
1081
+ const styleType = wAttr(style, "type") ?? "";
1082
+ if (!styleId || !["paragraph", "character", "table"].includes(styleType))
1083
+ continue;
1084
+ const name = wAttr(one(style, "w:name"), "val") ?? styleId;
1085
+ if (styleType !== "table" && ["Normal", "DefaultParagraphFont", "TableNormal"].includes(styleId))
1086
+ continue;
1087
+ if (styleId.endsWith("Tok") && styleType === "character")
1088
+ continue;
1089
+ const entry = { id: styleId, name };
1090
+ const bo = one(style, "w:basedOn");
1091
+ if (bo) {
1092
+ const v = wAttr(bo, "val");
1093
+ if (v && !["Normal", "DefaultParagraphFont"].includes(v))
1094
+ entry.basedOn = v;
1095
+ }
1096
+ const next = one(style, "w:next");
1097
+ if (next) {
1098
+ const v = wAttr(next, "val");
1099
+ if (v && v !== styleId)
1100
+ entry.next = v;
1101
+ }
1102
+ const link = one(style, "w:link");
1103
+ if (link) {
1104
+ const v = wAttr(link, "val");
1105
+ if (v)
1106
+ entry.link = v;
1107
+ }
1108
+ if (one(style, "w:qFormat"))
1109
+ entry.quickFormat = true;
1110
+ if (one(style, "w:semiHidden"))
1111
+ entry.semiHidden = true;
1112
+ if (one(style, "w:unhideWhenUsed"))
1113
+ entry.unhideWhenUsed = true;
1114
+ const ui = one(style, "w:uiPriority");
1115
+ if (ui) {
1116
+ const v = wNumber(ui);
1117
+ if (v !== null)
1118
+ entry.uiPriority = v;
1119
+ }
1120
+ if (styleType !== "table") {
1121
+ const run = mapRunProps(one(style, "w:rPr"));
1122
+ if (Object.keys(run).length)
1123
+ entry.run = run;
1124
+ const para = mapParagraphProps(one(style, "w:pPr"));
1125
+ if (Object.keys(para).length)
1126
+ entry.paragraph = para;
1127
+ }
1128
+ if (styleType === "table") {
1129
+ const tblPr = one(style, "w:tblPr");
1130
+ if (tblPr) {
1131
+ const tbl = mapTableProps(tblPr);
1132
+ if (tbl)
1133
+ entry.table = tbl;
1134
+ }
1135
+ const parts = [];
1136
+ for (const tsp of all(style, "w:tblStylePr")) {
1137
+ const mapped = mapTableStylePr(tsp);
1138
+ if (mapped)
1139
+ parts.push(mapped);
1140
+ }
1141
+ if (parts.length)
1142
+ entry.tableStyleParts = parts;
1143
+ const run = mapRunProps(one(style, "w:rPr"));
1144
+ if (Object.keys(run).length)
1145
+ entry.run = run;
1146
+ const pPr = mapParagraphProps(one(style, "w:pPr"));
1147
+ if (Object.keys(pPr).length)
1148
+ entry.paragraph = pPr;
1149
+ }
1150
+ if (styleType === "paragraph") {
1151
+ const key = BUILTIN_PARA[styleId];
1152
+ if (key) {
1153
+ const { id: _, ...rest } = entry;
1154
+ defaultOverrides[key] = rest;
1155
+ } else {
1156
+ paragraphStyles.push(entry);
1157
+ }
1158
+ } else if (styleType === "character") {
1159
+ const key = BUILTIN_CHAR[styleId];
1160
+ if (key) {
1161
+ const { id: _, ...rest } = entry;
1162
+ defaultOverrides[key] = rest;
1163
+ } else {
1164
+ characterStyles.push(entry);
1165
+ }
1166
+ } else if (styleType === "table") {
1167
+ tableStyles.push(entry);
1168
+ }
1169
+ }
1170
+ let tableXmlFragments = "";
1171
+ for (const el of all(xmlDocument, "//w:style[@w:type='table' and not(@w:default='1') and @w:customStyle='1']")) {
1172
+ const xmlStr = serializeElement(el);
1173
+ tableXmlFragments += xmlStr;
1174
+ }
1175
+ const result = {};
1176
+ if (Object.keys(defaultDoc).length > 0 || Object.keys(defaultOverrides).length > 0) {
1177
+ result.default = {
1178
+ ...Object.keys(defaultDoc).length > 0 ? { document: defaultDoc } : {},
1179
+ ...defaultOverrides
1180
+ };
1181
+ }
1182
+ if (paragraphStyles.length)
1183
+ result.paragraphStyles = paragraphStyles;
1184
+ if (characterStyles.length)
1185
+ result.characterStyles = characterStyles;
1186
+ if (tableStyles.length)
1187
+ result.tableStyles = tableStyles;
1188
+ if (tableXmlFragments)
1189
+ result.tableStylesXml = tableXmlFragments;
1190
+ return result;
1191
+ }
1192
+
1193
+ // src/commands/export.ts
1194
+ async function exportConfig(options) {
1195
+ const output = resolveOutputPath(options.output, "config.json", [".json"], "配置输出文件");
1196
+ prepareOutput(output, options.force ?? false);
1197
+ await copyFile(CONFIG_PATH, output);
1198
+ console.log(`已导出配置:${output}`);
1199
+ }
1200
+ async function exportStyle(options) {
1201
+ const source = options.file ? resolveInputPath(options.file, [".docx"], "DOCX 文件") : undefined;
1202
+ const defaultName = source ? `${parse3(source).name}_style.json` : "style.json";
1203
+ const output = resolveOutputPath(options.output, defaultName, [".json"], "样式输出文件");
1204
+ prepareOutput(output, options.force ?? false);
1205
+ if (source) {
1206
+ const styles = extractStylesFromDocx(source);
1207
+ writeFileSync3(output, `${JSON.stringify(styles, null, 2)}
1208
+ `, "utf-8");
1209
+ } else {
1210
+ writeFileSync3(output, readFileSync3(STYLE_CONFIG));
1211
+ }
1212
+ console.log(`已导出样式:${output}`);
1213
+ }
1214
+
1215
+ // src/commands/format.ts
1216
+ import { existsSync as existsSync4, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
1217
+ import { basename, dirname as dirname4, parse as parse4, resolve as resolve4 } from "node:path";
1218
+ async function formatMarkdown(options) {
1219
+ const input = resolveInputPath(options.file, [".md", ".markdown"], "Markdown 文件");
1220
+ const configPath = options.config ? resolveInputPath(options.config, [".json"], "配置文件") : CONFIG_PATH;
1221
+ const defaultName = `${parse4(input).name}_formatted.md`;
1222
+ const output = resolveOutputPath(options.output, defaultName, [".md", ".markdown"], "Markdown 输出文件");
1223
+ const force = options.force ?? false;
1224
+ prepareOutput(output, force);
1225
+ const config = await loadConfig(configPath);
1226
+ const outputName = parse4(output).name;
1227
+ const assetsName = `${outputName}_assets`;
1228
+ const assetsDir = resolve4(dirname4(output), assetsName);
1229
+ if (config.renderMermaid.enabled && existsSync4(assetsDir)) {
1230
+ if (!force) {
1231
+ throw new Error(`资源目录已存在:${assetsDir}
1232
+ 使用 --force 覆盖现有目录`);
1233
+ }
1234
+ const parent = resolve4(dirname4(output));
1235
+ if (dirname4(assetsDir) !== parent)
1236
+ throw new Error(`拒绝清理非输出目录:${assetsDir}`);
1237
+ rmSync(assetsDir, { recursive: true, force: true });
1238
+ }
1239
+ const formatted = await preprocess(input, config, assetsDir, basename(assetsDir));
1240
+ writeFileSync4(output, formatted, "utf-8");
1241
+ console.log(`已格式化:${output}`);
1242
+ }
1243
+
1244
+ // src/index.ts
1245
+ async function run(args) {
1246
+ const { version } = JSON.parse(await readFile3(resolve5(PKG_DIR, "package.json"), "utf-8"));
1247
+ const program = createProgram(version, {
1248
+ convert: convertMarkdown,
1249
+ format: formatMarkdown,
1250
+ exportConfig,
1251
+ exportStyle
1252
+ });
1253
+ program.exitOverride();
1254
+ try {
1255
+ await program.parseAsync(args, { from: "user" });
1256
+ return 0;
1257
+ } catch (error) {
1258
+ if (error instanceof CommanderError)
1259
+ return error.exitCode;
1260
+ const message = error instanceof Error ? error.message : String(error);
1261
+ console.error(`错误:${message}`);
1262
+ console.error("使用 --help 查看帮助");
1263
+ return 1;
1264
+ }
1265
+ }
1266
+ var entryPath = process.argv[1];
1267
+ if (entryPath && realpathSync(entryPath) === realpathSync(fileURLToPath2(import.meta.url))) {
1268
+ process.exitCode = await run(process.argv.slice(2));
1269
+ }
1270
+ export {
1271
+ run
1272
+ };