md2xhs 1.0.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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 资源加载器 - 从本地读取 CDN 资源
3
+ */
4
+ export declare class AssetLoader {
5
+ private static assetsDir;
6
+ private static cache;
7
+ /**
8
+ * 读取本地资源文件
9
+ */
10
+ static loadAsset(filename: string): string;
11
+ /**
12
+ * 获取 KaTeX CSS
13
+ */
14
+ static getKatexCss(): string;
15
+ /**
16
+ * 获取 KaTeX JS
17
+ */
18
+ static getKatexJs(): string;
19
+ /**
20
+ * 获取 KaTeX Auto-render JS
21
+ */
22
+ static getKatexAutoRender(): string;
23
+ /**
24
+ * 获取 Mermaid JS
25
+ */
26
+ static getMermaidJs(): string;
27
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AssetLoader = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ /**
7
+ * 资源加载器 - 从本地读取 CDN 资源
8
+ */
9
+ class AssetLoader {
10
+ /**
11
+ * 读取本地资源文件
12
+ */
13
+ static loadAsset(filename) {
14
+ if (this.cache.has(filename)) {
15
+ return this.cache.get(filename);
16
+ }
17
+ try {
18
+ const filePath = (0, path_1.join)(this.assetsDir, filename);
19
+ const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
20
+ this.cache.set(filename, content);
21
+ return content;
22
+ }
23
+ catch (error) {
24
+ console.warn(`无法加载本地资源 ${filename}, 将使用 CDN`);
25
+ return '';
26
+ }
27
+ }
28
+ /**
29
+ * 获取 KaTeX CSS
30
+ */
31
+ static getKatexCss() {
32
+ return this.loadAsset('katex.min.css');
33
+ }
34
+ /**
35
+ * 获取 KaTeX JS
36
+ */
37
+ static getKatexJs() {
38
+ return this.loadAsset('katex.min.js');
39
+ }
40
+ /**
41
+ * 获取 KaTeX Auto-render JS
42
+ */
43
+ static getKatexAutoRender() {
44
+ return this.loadAsset('auto-render.min.js');
45
+ }
46
+ /**
47
+ * 获取 Mermaid JS
48
+ */
49
+ static getMermaidJs() {
50
+ return this.loadAsset('mermaid.min.js');
51
+ }
52
+ }
53
+ exports.AssetLoader = AssetLoader;
54
+ AssetLoader.assetsDir = (0, path_1.join)(__dirname, 'assets');
55
+ AssetLoader.cache = new Map();
@@ -0,0 +1,87 @@
1
+ import { Md2ImageOptions, ConvertResult } from './types';
2
+ /**
3
+ * Markdown 转图片转换器
4
+ */
5
+ export declare class Md2ImageConverter {
6
+ private browser;
7
+ private options;
8
+ constructor(options?: Md2ImageOptions);
9
+ /**
10
+ * 初始化浏览器
11
+ */
12
+ private initBrowser;
13
+ /**
14
+ * 关闭浏览器
15
+ */
16
+ close(): Promise<void>;
17
+ /**
18
+ * 获取背景样式
19
+ */
20
+ private getBackgroundStyle;
21
+ /**
22
+ * 计算目标高度(根据模式)
23
+ */
24
+ private calculateHeight;
25
+ /**
26
+ * 预处理 Markdown (支持卡片语法)
27
+ */
28
+ private preprocessMarkdown;
29
+ /**
30
+ * 生成扩展功能的脚本
31
+ */
32
+ private generateExtensionScripts;
33
+ /**
34
+ * 生成卡片样式
35
+ */
36
+ private generateCardStyles;
37
+ /**
38
+ * 生成完整的 HTML
39
+ */
40
+ private generateHTML;
41
+ /**
42
+ * 转换 Markdown 为图片
43
+ */
44
+ convert(markdown: string, outputPath?: string): Promise<ConvertResult>;
45
+ /**
46
+ * 批量转换
47
+ */
48
+ convertBatch(items: Array<{
49
+ markdown: string;
50
+ outputPath: string;
51
+ }>): Promise<ConvertResult[]>;
52
+ /**
53
+ * 从文件转换
54
+ * @param filePath Markdown 文件路径
55
+ * @param outputPath 可选,输出路径(不提供则生成同名 .png 文件)
56
+ */
57
+ convertFromFile(filePath: string, outputPath?: string): Promise<ConvertResult>;
58
+ /**
59
+ * 从目录批量转换
60
+ * @param dirPath 目录路径
61
+ * @param outputDir 可选,输出目录(不提供则输出到原文件所在目录)
62
+ * @param recursive 是否递归处理子目录,默认 false
63
+ */
64
+ convertFromDirectory(dirPath: string, outputDir?: string, recursive?: boolean): Promise<ConvertResult[]>;
65
+ /**
66
+ * 更新配置
67
+ */
68
+ updateOptions(options: Partial<Md2ImageOptions>): void;
69
+ /**
70
+ * 获取当前配置
71
+ */
72
+ getOptions(): Required<Md2ImageOptions>;
73
+ }
74
+ /**
75
+ * 便捷函数:快速转换
76
+ * 支持 Markdown 字符串或文件路径
77
+ * @param input Markdown 字符串或文件路径
78
+ * @param options 转换选项
79
+ */
80
+ export declare function md2image(input: string, options?: Md2ImageOptions): Promise<ConvertResult>;
81
+ /**
82
+ * 便捷函数:批量转换目录
83
+ * @param dirPath 目录路径
84
+ * @param options 转换选项
85
+ * @param recursive 是否递归处理子目录
86
+ */
87
+ export declare function md2imageDir(dirPath: string, options?: Md2ImageOptions, recursive?: boolean): Promise<ConvertResult[]>;
@@ -0,0 +1,421 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Md2ImageConverter = void 0;
7
+ exports.md2image = md2image;
8
+ exports.md2imageDir = md2imageDir;
9
+ const marked_1 = require("marked");
10
+ const puppeteer_1 = __importDefault(require("puppeteer"));
11
+ const path_1 = require("path");
12
+ const types_1 = require("./types");
13
+ const styles_1 = require("./styles");
14
+ const assets_loader_1 = require("./assets-loader");
15
+ const file_utils_1 = require("./file-utils");
16
+ /**
17
+ * Markdown 转图片转换器
18
+ */
19
+ class Md2ImageConverter {
20
+ constructor(options = {}) {
21
+ this.browser = null;
22
+ // 设置默认选项
23
+ this.options = {
24
+ mode: options.mode || types_1.ExportMode.XHS,
25
+ width: options.width || 640,
26
+ padding: options.padding || 40,
27
+ fontSize: options.fontSize || 18,
28
+ backgroundPreset: options.backgroundPreset || 'purple',
29
+ customBackground: options.customBackground,
30
+ scale: options.scale || 3,
31
+ outputPath: options.outputPath || (0, path_1.join)(process.cwd(), 'output.png'),
32
+ extensions: {
33
+ enableMath: options.extensions?.enableMath !== false,
34
+ enableDiagram: options.extensions?.enableDiagram !== false,
35
+ enableCard: options.extensions?.enableCard !== false,
36
+ },
37
+ };
38
+ // 配置 marked
39
+ marked_1.marked.setOptions({
40
+ breaks: true,
41
+ gfm: true,
42
+ });
43
+ }
44
+ /**
45
+ * 初始化浏览器
46
+ */
47
+ async initBrowser() {
48
+ if (!this.browser) {
49
+ this.browser = await puppeteer_1.default.launch({
50
+ headless: true,
51
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
52
+ });
53
+ }
54
+ }
55
+ /**
56
+ * 关闭浏览器
57
+ */
58
+ async close() {
59
+ if (this.browser) {
60
+ await this.browser.close();
61
+ this.browser = null;
62
+ }
63
+ }
64
+ /**
65
+ * 获取背景样式
66
+ */
67
+ getBackgroundStyle() {
68
+ if (this.options.customBackground) {
69
+ return (0, styles_1.generateCustomBackground)(this.options.customBackground);
70
+ }
71
+ return styles_1.BACKGROUND_PRESETS[this.options.backgroundPreset];
72
+ }
73
+ /**
74
+ * 计算目标高度(根据模式)
75
+ */
76
+ calculateHeight(mode, width) {
77
+ switch (mode) {
78
+ case types_1.ExportMode.XHS:
79
+ case 'xhs':
80
+ // 小红书 3:4 比例
81
+ return Math.round((width / 3) * 4);
82
+ case types_1.ExportMode.PYQ:
83
+ case 'pyq':
84
+ // 朋友圈 1290:2796 比例
85
+ return Math.round(width * (2796 / 1290));
86
+ case types_1.ExportMode.FREE:
87
+ case 'free':
88
+ default:
89
+ return null; // 自适应高度
90
+ }
91
+ }
92
+ /**
93
+ * 预处理 Markdown (支持卡片语法)
94
+ */
95
+ preprocessMarkdown(markdown) {
96
+ let processed = markdown;
97
+ // 处理卡片语法 :::card
98
+ if (this.options.extensions.enableCard) {
99
+ processed = processed.replace(/:::card(?:\s+(info|success|warning|error))?\s*\n([\s\S]*?)\n:::/g, (_match, type, content) => {
100
+ const cardClass = type ? `card-${type}` : '';
101
+ return `<div class="madopic-card ${cardClass}">\n<div class="card-content">\n\n${content}\n\n</div>\n</div>`;
102
+ });
103
+ }
104
+ return processed;
105
+ }
106
+ /**
107
+ * 生成扩展功能的脚本
108
+ */
109
+ generateExtensionScripts() {
110
+ const scripts = [];
111
+ // KaTeX 数学公式支持
112
+ if (this.options.extensions.enableMath) {
113
+ scripts.push(`
114
+ <script>
115
+ // 等待 KaTeX 加载完成后渲染数学公式
116
+ window.addEventListener('load', function() {
117
+ if (typeof renderMathInElement !== 'undefined') {
118
+ renderMathInElement(document.body, {
119
+ delimiters: [
120
+ {left: '$$', right: '$$', display: true},
121
+ {left: '$', right: '$', display: false},
122
+ {left: '\\\\[', right: '\\\\]', display: true},
123
+ {left: '\\\\(', right: '\\\\)', display: false}
124
+ ],
125
+ throwOnError: false
126
+ });
127
+ }
128
+ });
129
+ </script>
130
+ `);
131
+ }
132
+ // Mermaid 图表支持
133
+ if (this.options.extensions.enableDiagram) {
134
+ scripts.push(`
135
+ <script>
136
+ // 初始化 Mermaid
137
+ if (typeof mermaid !== 'undefined') {
138
+ mermaid.initialize({
139
+ startOnLoad: true,
140
+ theme: 'default',
141
+ themeVariables: {
142
+ primaryColor: '#6366f1',
143
+ primaryTextColor: '#1f2937',
144
+ primaryBorderColor: '#4f46e5',
145
+ lineColor: '#6b7280',
146
+ secondaryColor: '#f3f4f6',
147
+ tertiaryColor: '#ffffff'
148
+ }
149
+ });
150
+ }
151
+ </script>
152
+ `);
153
+ }
154
+ return scripts.join('\n');
155
+ }
156
+ /**
157
+ * 生成卡片样式
158
+ */
159
+ generateCardStyles() {
160
+ if (!this.options.extensions.enableCard) {
161
+ return '';
162
+ }
163
+ return `
164
+ .madopic-card {
165
+ margin: 20px 0;
166
+ padding: 20px 24px;
167
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
168
+ border: 1px solid rgba(99, 102, 241, 0.2);
169
+ border-radius: 10px;
170
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
171
+ transition: all 0.3s ease;
172
+ }
173
+
174
+ .madopic-card .card-content {
175
+ color: var(--text-light);
176
+ line-height: 1.7;
177
+ }
178
+
179
+ .madopic-card.card-info {
180
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(29, 78, 216, 0.12) 100%);
181
+ border-color: rgba(59, 130, 246, 0.2);
182
+ }
183
+
184
+ .madopic-card.card-success {
185
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%);
186
+ border-color: rgba(16, 185, 129, 0.2);
187
+ }
188
+
189
+ .madopic-card.card-warning {
190
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(217, 119, 6, 0.12) 100%);
191
+ border-color: rgba(245, 158, 11, 0.2);
192
+ }
193
+
194
+ .madopic-card.card-error {
195
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.12) 100%);
196
+ border-color: rgba(239, 68, 68, 0.2);
197
+ }
198
+ `;
199
+ }
200
+ /**
201
+ * 生成完整的 HTML
202
+ */
203
+ generateHTML(markdownContent) {
204
+ // 预处理 Markdown
205
+ const preprocessed = this.preprocessMarkdown(markdownContent);
206
+ const htmlContent = marked_1.marked.parse(preprocessed);
207
+ const backgroundStyle = this.getBackgroundStyle();
208
+ const fontSizeVars = (0, styles_1.generateFontSizeVars)(this.options.fontSize);
209
+ const targetHeight = this.calculateHeight(this.options.mode, this.options.width);
210
+ const posterHeightStyle = targetHeight
211
+ ? `height: ${targetHeight}px; min-height: ${targetHeight}px; overflow: hidden;`
212
+ : 'min-height: 600px;';
213
+ const contentMaxHeightStyle = targetHeight
214
+ ? `max-height: ${targetHeight - this.options.padding * 2}px; overflow: hidden;`
215
+ : '';
216
+ // 生成内联资源
217
+ const inlineAssets = [];
218
+ const inlineScripts = [];
219
+ if (this.options.extensions.enableMath) {
220
+ // 内联 KaTeX CSS
221
+ const katexCss = assets_loader_1.AssetLoader.getKatexCss();
222
+ if (katexCss) {
223
+ inlineAssets.push(`<style>${katexCss}</style>`);
224
+ }
225
+ // 内联 KaTeX JS
226
+ const katexJs = assets_loader_1.AssetLoader.getKatexJs();
227
+ const autoRenderJs = assets_loader_1.AssetLoader.getKatexAutoRender();
228
+ if (katexJs && autoRenderJs) {
229
+ inlineScripts.push(`<script>${katexJs}</script>`);
230
+ inlineScripts.push(`<script>${autoRenderJs}</script>`);
231
+ }
232
+ }
233
+ if (this.options.extensions.enableDiagram) {
234
+ // 内联 Mermaid JS
235
+ const mermaidJs = assets_loader_1.AssetLoader.getMermaidJs();
236
+ if (mermaidJs) {
237
+ inlineScripts.push(`<script>${mermaidJs}</script>`);
238
+ }
239
+ }
240
+ return `
241
+ <!DOCTYPE html>
242
+ <html lang="zh-CN">
243
+ <head>
244
+ <meta charset="UTF-8">
245
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
246
+ <title>Markdown to Image</title>
247
+ ${inlineAssets.join('\n ')}
248
+ ${inlineScripts.join('\n ')}
249
+ <style>
250
+ ${styles_1.FULL_STYLES}
251
+ ${this.generateCardStyles()}
252
+ </style>
253
+ </head>
254
+ <body>
255
+ <div class="markdown-poster" style="
256
+ background: ${backgroundStyle};
257
+ width: ${this.options.width}px;
258
+ padding: ${this.options.padding}px;
259
+ ${posterHeightStyle}
260
+ ">
261
+ <div class="poster-content" style="
262
+ ${fontSizeVars}
263
+ ${contentMaxHeightStyle}
264
+ ">
265
+ ${htmlContent}
266
+ </div>
267
+ </div>
268
+ ${this.generateExtensionScripts()}
269
+ </body>
270
+ </html>
271
+ `.trim();
272
+ }
273
+ /**
274
+ * 转换 Markdown 为图片
275
+ */
276
+ async convert(markdown, outputPath) {
277
+ await this.initBrowser();
278
+ if (!this.browser) {
279
+ throw new Error('浏览器初始化失败');
280
+ }
281
+ const page = await this.browser.newPage();
282
+ try {
283
+ // 生成 HTML 内容
284
+ const htmlContent = this.generateHTML(markdown);
285
+ // 设置页面内容
286
+ await page.setContent(htmlContent, {
287
+ waitUntil: 'networkidle0',
288
+ });
289
+ // 等待渲染完成
290
+ await page.waitForSelector('.markdown-poster', { timeout: 5000 });
291
+ // 如果启用了扩展功能,等待额外时间确保渲染完成
292
+ if (this.options.extensions.enableMath || this.options.extensions.enableDiagram) {
293
+ // 等待 1.5 秒确保 KaTeX 和 Mermaid 渲染完成
294
+ await new Promise(resolve => setTimeout(resolve, 1500));
295
+ }
296
+ // 获取元素尺寸
297
+ const element = await page.$('.markdown-poster');
298
+ if (!element) {
299
+ throw new Error('无法找到 .markdown-poster 元素');
300
+ }
301
+ const boundingBox = await element.boundingBox();
302
+ if (!boundingBox) {
303
+ throw new Error('无法获取元素边界框');
304
+ }
305
+ // 截图
306
+ const finalOutputPath = outputPath || this.options.outputPath;
307
+ // 设置设备像素比以提高清晰度
308
+ await page.setViewport({
309
+ width: this.options.width,
310
+ height: boundingBox.height,
311
+ deviceScaleFactor: this.options.scale,
312
+ });
313
+ await element.screenshot({
314
+ path: finalOutputPath,
315
+ type: 'png',
316
+ omitBackground: false,
317
+ });
318
+ return {
319
+ imagePath: finalOutputPath,
320
+ width: Math.round(boundingBox.width),
321
+ height: Math.round(boundingBox.height),
322
+ };
323
+ }
324
+ finally {
325
+ await page.close();
326
+ }
327
+ }
328
+ /**
329
+ * 批量转换
330
+ */
331
+ async convertBatch(items) {
332
+ await this.initBrowser();
333
+ const results = [];
334
+ for (const item of items) {
335
+ const result = await this.convert(item.markdown, item.outputPath);
336
+ results.push(result);
337
+ }
338
+ return results;
339
+ }
340
+ /**
341
+ * 从文件转换
342
+ * @param filePath Markdown 文件路径
343
+ * @param outputPath 可选,输出路径(不提供则生成同名 .png 文件)
344
+ */
345
+ async convertFromFile(filePath, outputPath) {
346
+ const markdown = await (0, file_utils_1.readMarkdownFile)(filePath);
347
+ const finalOutputPath = outputPath || (0, file_utils_1.generateOutputPath)(filePath);
348
+ return this.convert(markdown, finalOutputPath);
349
+ }
350
+ /**
351
+ * 从目录批量转换
352
+ * @param dirPath 目录路径
353
+ * @param outputDir 可选,输出目录(不提供则输出到原文件所在目录)
354
+ * @param recursive 是否递归处理子目录,默认 false
355
+ */
356
+ async convertFromDirectory(dirPath, outputDir, recursive = false) {
357
+ const markdownFiles = await (0, file_utils_1.getMarkdownFiles)(dirPath, recursive);
358
+ const results = [];
359
+ for (const filePath of markdownFiles) {
360
+ const outputPath = (0, file_utils_1.generateOutputPath)(filePath, outputDir);
361
+ const result = await this.convertFromFile(filePath, outputPath);
362
+ results.push(result);
363
+ }
364
+ return results;
365
+ }
366
+ /**
367
+ * 更新配置
368
+ */
369
+ updateOptions(options) {
370
+ this.options = {
371
+ ...this.options,
372
+ ...options,
373
+ };
374
+ }
375
+ /**
376
+ * 获取当前配置
377
+ */
378
+ getOptions() {
379
+ return { ...this.options };
380
+ }
381
+ }
382
+ exports.Md2ImageConverter = Md2ImageConverter;
383
+ /**
384
+ * 便捷函数:快速转换
385
+ * 支持 Markdown 字符串或文件路径
386
+ * @param input Markdown 字符串或文件路径
387
+ * @param options 转换选项
388
+ */
389
+ async function md2image(input, options) {
390
+ const converter = new Md2ImageConverter(options);
391
+ try {
392
+ // 检测输入是文件路径还是 Markdown 内容
393
+ const isFilePath = await (0, file_utils_1.isFile)(input);
394
+ if (isFilePath) {
395
+ // 如果是文件路径
396
+ return await converter.convertFromFile(input, options?.outputPath);
397
+ }
398
+ else {
399
+ // 否则当作 Markdown 内容
400
+ return await converter.convert(input);
401
+ }
402
+ }
403
+ finally {
404
+ await converter.close();
405
+ }
406
+ }
407
+ /**
408
+ * 便捷函数:批量转换目录
409
+ * @param dirPath 目录路径
410
+ * @param options 转换选项
411
+ * @param recursive 是否递归处理子目录
412
+ */
413
+ async function md2imageDir(dirPath, options, recursive = false) {
414
+ const converter = new Md2ImageConverter(options);
415
+ try {
416
+ return await converter.convertFromDirectory(dirPath, options?.outputPath, recursive);
417
+ }
418
+ finally {
419
+ await converter.close();
420
+ }
421
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 扩展功能配置
3
+ */
4
+ export interface ExtensionOptions {
5
+ /**
6
+ * 是否启用数学公式支持 (KaTeX)
7
+ * @default false
8
+ */
9
+ enableMath?: boolean;
10
+ /**
11
+ * 是否启用图表支持 (Mermaid)
12
+ * @default false
13
+ */
14
+ enableDiagram?: boolean;
15
+ /**
16
+ * 是否启用卡片语法
17
+ * @default true
18
+ */
19
+ enableCard?: boolean;
20
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 判断路径是否为文件
3
+ */
4
+ export declare function isFile(path: string): Promise<boolean>;
5
+ /**
6
+ * 判断路径是否为目录
7
+ */
8
+ export declare function isDirectory(path: string): Promise<boolean>;
9
+ /**
10
+ * 读取 Markdown 文件内容
11
+ */
12
+ export declare function readMarkdownFile(filePath: string): Promise<string>;
13
+ /**
14
+ * 获取目录中所有的 Markdown 文件
15
+ */
16
+ export declare function getMarkdownFiles(dirPath: string, recursive?: boolean): Promise<string[]>;
17
+ /**
18
+ * 生成输出文件路径
19
+ * 例如: './docs/README.md' -> './docs/README.png'
20
+ */
21
+ export declare function generateOutputPath(inputPath: string, outputDir?: string): string;