@v1hz/md2docx 1.6.0 → 2.1.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.
@@ -1,146 +0,0 @@
1
- import { type Root } from "mdast";
2
- import { writeFileSync, mkdirSync, existsSync } from "fs";
3
- import { join } from "path";
4
- import { renderMermaidSVG, THEMES } from "beautiful-mermaid";
5
- import sharp from "sharp";
6
-
7
- export async function renderMermaid(root: Root, outDir: string, theme: string, density: number) {
8
- if (!existsSync(outDir)) {
9
- mkdirSync(outDir, { recursive: true });
10
- }
11
-
12
- const mermaidTheme = THEMES[theme as keyof typeof THEMES] ?? THEMES["tokyo-night-light"];
13
-
14
- let counter = 0;
15
- for (let i = 0; i < root.children.length; i++) {
16
- const child = root.children[i]!;
17
- if (child?.type !== "code" || child.lang !== "mermaid") continue;
18
-
19
- counter++;
20
- const pngFile = join(outDir, `mermaid_${counter}.png`);
21
- const title = getMermaidTitle(child.meta);
22
-
23
- try {
24
- let svg = renderMermaidSVG(child.value, mermaidTheme);
25
- svg = resolveCSSVars(svg);
26
- const png = await sharp(Buffer.from(svg), {
27
- density,
28
- })
29
- .png()
30
- .toBuffer();
31
- writeFileSync(pngFile, png);
32
- } catch (err) {
33
- console.error(`mermaid 渲染失败 (#${counter}):`, err);
34
- continue;
35
- }
36
-
37
- root.children[i] = {
38
- type: "paragraph",
39
- children: [
40
- {
41
- type: "image",
42
- url: pngFile,
43
- title: title ?? undefined,
44
- alt: title ?? "",
45
- },
46
- ],
47
- } as unknown as never;
48
- }
49
- }
50
-
51
- /** 从 mermaid code 的 meta 中提取 title="..." */
52
- function getMermaidTitle(meta: string | null | undefined): string | null {
53
- if (!meta) return null;
54
- const match = meta.match(/title\s*=\s*["']([^"']+)["']/);
55
- return match ? match[1]! : null;
56
- }
57
-
58
- interface MixColors {
59
- c1: string;
60
- c2: string;
61
- pct: number;
62
- }
63
-
64
- /**
65
- * 将 SVG 中的 CSS 自定义属性(var())和 color-mix() 内联为
66
- * sharp/librsvg 能理解的具体十六进制颜色值。
67
- */
68
- function resolveCSSVars(svg: string): string {
69
- const vars: Record<string, string> = {};
70
-
71
- // 1. 从 <svg style="--bg:#...;--fg:#..."> 提取基础变量
72
- const styleAttr = svg.match(/<svg[^>]*style="([^"]+)"/)?.[1];
73
- if (!styleAttr) return svg;
74
- styleAttr.replace(/--([\w-]+)\s*:\s*([^;]+)/g, (_, n, v) => {
75
- vars[n] = v.trim();
76
- return "";
77
- });
78
-
79
- // 2. 从 <style> 块提取派生变量(svg { --_text: var(--fg); ... })
80
- const styleBlock = svg.match(/<style>([\s\S]*?)<\/style>/)?.[1];
81
- if (styleBlock) {
82
- const svgRules = styleBlock.match(/svg\s*\{([^}]+)\}/)?.[1];
83
- if (svgRules) {
84
- svgRules.replace(/--([\w-]+)\s*:\s*([^;]+)/g, (_, n, v) => {
85
- vars[n] = v.trim();
86
- return "";
87
- });
88
- }
89
- }
90
-
91
- // 3. 解析所有 var() 引用和 color-mix()
92
- let changed = true;
93
- while (changed) {
94
- changed = false;
95
- for (const key of Object.keys(vars)) {
96
- const newVal = vars[key]!.replace(/var\(--([\w-]+)\)/g, (_, name) => {
97
- const resolved = vars[name];
98
- if (resolved) {
99
- changed = true;
100
- return resolved;
101
- }
102
- return `var(--${name})`;
103
- });
104
- vars[key] = newVal;
105
- }
106
- }
107
-
108
- // 4. 解析 color-mix(in srgb, <color> X%, <color>)
109
- function resolveColorMix(expr: string): string {
110
- const colors = getMixColors(expr);
111
- if (!colors) return expr;
112
- return blendColors(colors.c1, colors.c2, colors.pct);
113
- }
114
-
115
- for (const key of Object.keys(vars)) {
116
- vars[key] = vars[key]!.replace(/color-mix\([^)]+\)/g, (m) => resolveColorMix(m));
117
- }
118
-
119
- // 5. 全局替换所有剩余的 var() 为具体值
120
- return svg.replace(/var\(--([\w-]+)\)/g, (_, name) => {
121
- return vars[name] ?? `var(--${name})`;
122
- });
123
- }
124
-
125
- /** 解析 color-mix(in srgb, <color> <pct>%, <color>) */
126
- function getMixColors(expr: string): MixColors | null {
127
- const match = expr.match(
128
- /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*\)/,
129
- );
130
- if (!match) return null;
131
- return { c1: match[1]!, c2: match[3]!, pct: parseFloat(match[2]!) / 100 };
132
- }
133
-
134
- /** 混合两个十六进制颜色,pct 是 c1 的权重 */
135
- function blendColors(c1: string, c2: string, pct: number): string {
136
- const r1 = Number.parseInt(c1.slice(1, 3), 16);
137
- const g1 = Number.parseInt(c1.slice(3, 5), 16);
138
- const b1 = Number.parseInt(c1.slice(5, 7), 16);
139
- const r2 = Number.parseInt(c2.slice(1, 3), 16);
140
- const g2 = Number.parseInt(c2.slice(3, 5), 16);
141
- const b2 = Number.parseInt(c2.slice(5, 7), 16);
142
- const r = Math.round(r1 * pct + r2 * (1 - pct));
143
- const g = Math.round(g1 * pct + g2 * (1 - pct));
144
- const b = Math.round(b1 * pct + b2 * (1 - pct));
145
- return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
146
- }
@@ -1,144 +0,0 @@
1
- import { type Heading, type Root, type Text, type Yaml } from "mdast";
2
- import { type HeadingNumberingConfig, type TitleConfig } from "../config";
3
-
4
- export function addTitle(fileName: string, root: Root, headings: Heading[], config: TitleConfig) {
5
- const { enabled, strategy } = config;
6
- if (!enabled || strategy === "none") return;
7
-
8
- const hasFrontmatterTitle = root.children.some(
9
- (c): c is Yaml => c.type === "yaml" && /^title:/m.test(c.value),
10
- );
11
- if (hasFrontmatterTitle) return;
12
-
13
- if (strategy === "filename") {
14
- const fallbackTitle = fileName.replace(/\.\w+$/, "");
15
- root.children.unshift({ type: "yaml", value: `title: ${fallbackTitle}` });
16
- return;
17
- }
18
-
19
- const depth1 = headings.filter((n) => n.depth === 1);
20
- const firstDepth1 = depth1[0];
21
-
22
- if (strategy === "single-h1") {
23
- if (firstDepth1 != null && depth1.length === 1) {
24
- const titleText = firstDepth1.children
25
- .filter((c): c is Text => c.type === "text")
26
- .map((c) => c.value)
27
- .join("");
28
-
29
- const idx = root.children.indexOf(firstDepth1);
30
- if (idx !== -1) root.children.splice(idx, 1);
31
- headings.splice(headings.indexOf(firstDepth1), 1);
32
-
33
- root.children.unshift({ type: "yaml", value: `title: ${titleText}` });
34
- return;
35
- }
36
- }
37
-
38
- // strategy === "first-h1"
39
- if (firstDepth1 != null && depth1.length === 1 && firstDepth1 === headings[0]) {
40
- const titleText = firstDepth1.children
41
- .filter((c): c is Text => c.type === "text")
42
- .map((c) => c.value)
43
- .join("");
44
-
45
- const idx = root.children.indexOf(firstDepth1);
46
- if (idx !== -1) root.children.splice(idx, 1);
47
- headings.splice(headings.indexOf(firstDepth1), 1);
48
-
49
- root.children.unshift({ type: "yaml", value: `title: ${titleText}` });
50
- return;
51
- }
52
-
53
- const fallbackTitle = fileName.replace(/\.\w+$/, "");
54
- root.children.unshift({ type: "yaml", value: `title: ${fallbackTitle}` });
55
- }
56
-
57
- export function normalizeHeadings(nodes: Heading[]) {
58
- if (nodes.length === 0) return;
59
-
60
- let minDepth = 6;
61
- for (const node of nodes) {
62
- minDepth = Math.min(minDepth, node.depth);
63
- }
64
-
65
- const offset = minDepth - 1;
66
- for (let i = 0; i < nodes.length; i++) {
67
- nodes[i]!.depth -= offset;
68
- }
69
-
70
- for (let i = 1; i < nodes.length; i++) {
71
- if (nodes[i]!.depth > nodes[i - 1]!.depth + 1) {
72
- nodes[i]!.depth = (nodes[i - 1]!.depth + 1) as 1 | 2 | 3 | 4 | 5 | 6;
73
- }
74
- }
75
- }
76
-
77
- export function numberHeadings(nodes: Heading[], config: HeadingNumberingConfig) {
78
- if (nodes.length === 0) return;
79
- const { detectExisting, existingPattern } = config;
80
- const counter: number[] = [0, 0, 0, 0, 0, 0];
81
- for (let i = 0; i <= nodes[0]!.depth - 2; ++i) {
82
- counter[i] = 1;
83
- }
84
- for (const node of nodes) {
85
- const d = node.depth;
86
- counter[d - 1]!++;
87
- counter.fill(0, d);
88
- const prefix = buildPrefix(counter);
89
- const first = node.children[0];
90
- if (first?.type === "text") {
91
- const text = detectExisting
92
- ? stripHeadingNum(first.value, existingPattern, config?.useBuiltinRules)
93
- : first.value;
94
- first.value = text ? `${prefix} ${text}` : prefix;
95
- } else {
96
- node.children.unshift({
97
- type: "text",
98
- value: prefix,
99
- });
100
- }
101
- }
102
- }
103
-
104
- function buildPrefix(counter: number[]): string {
105
- const end = counter.findLastIndex((x) => x !== 0);
106
- return counter.slice(0, end + 1).join(".");
107
- }
108
-
109
- /**
110
- * 匹配形如 "1.2.3"、"1.2.3." 的数字序号前缀,末尾可选空格。
111
- * 注意:只匹配至少两段或带结尾点的,避免与公共版本号冲突。
112
- */
113
- const RE_DOT_NUM = /^\d+(\.\d+)+\.?\s*/;
114
-
115
- /** 匹配形如 "(一)"、"(一)" 的中文括号序号前缀 */
116
- const RE_CN_PAREN = /^[((][一二三四五六七八九十百千]+[))]\s*/;
117
-
118
- /** 匹配形如 "一、"、"一." 的中文数字单级序号前缀 */
119
- const RE_CN_NUM = /^[一二三四五六七八九十百千]+[、.]\s*/;
120
-
121
- /**
122
- * 匹配形如 "一、二、三" 或 "一、二、三、" 的中文顿号多级序号前缀。
123
- * 注意此正则须在 RE_CN_NUM 之后测试,以避免单级被优先匹配。
124
- */
125
- const RE_CN_DUN = /^[一二三四五六七八九十百千]+(?:、[一二三四五六七八九十百千]+)+、?\s*/;
126
-
127
- /** 去掉 heading 文本中已有的编号前缀 */
128
- function stripHeadingNum(text: string, customPattern?: string, useBuiltinRules?: boolean): string {
129
- let result = text;
130
- if (customPattern) {
131
- result = result.replace(new RegExp(`^${customPattern}`), "");
132
- }
133
- if (useBuiltinRules !== false) {
134
- result = result.replace(
135
- new RegExp(
136
- `^(?:${[RE_CN_DUN.source, RE_CN_PAREN.source, RE_CN_NUM.source, RE_DOT_NUM.source].join(
137
- "|",
138
- )})`,
139
- ),
140
- "",
141
- );
142
- }
143
- return result;
144
- }