UNPKG

koishi-plugin-markdown-to-image-service

Version:

Koishi 的 Markdown 转图片服务插件。支持 LaTeX、Mermaid、代码高亮等。

343 lines (324 loc) 11.4 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_markdown_it = __toESM(require("markdown-it")); var import_markdown_it_katex = __toESM(require("markdown-it-katex")); var import_markdown_it_highlightjs = __toESM(require("markdown-it-highlightjs")); var inject = { required: ["puppeteer"], optional: ["markdownToImage"] }; var name = "markdown-to-image-service"; var usage = `## 命令 ### \`markdownToImage [markdownText:text]\` 将 Markdown 文本转换为图片。 - **用法 1**:\`markdownToImage # Hello World\` - **用法 2**:直接输入 \`markdownToImage\`,根据提示输入 Markdown 内容。 ## 服务 本插件提供 \`markdownToImage\` 服务,供其他插件调用。 \`\`\`typescript ctx.markdownToImage.convertToImage(markdownText: string): Promise<Buffer> \`\`\` ### 示例 \`\`\`typescript import { Context, h } from 'koishi' import {} from 'koishi-plugin-markdown-to-image-service' export const inject = { required: ['markdownToImage'], } export function apply(ctx: Context) { ctx.command('test-md', '测试 Markdown 图片转换') .action(async ({ session }) => { const markdown = \` # Hello, Koishi This is a test. - LaTeX: $E=mc^2$ - Code: \\\`\\\`\\\`typescript console.log('Hello, world!') \\\`\\\`\\\` - Mermaid: \\\`\\\`\\\`mermaid graph TD; A-->B; A-->C; B-->D; C-->D; \\\`\\\`\\\` \` const imageBuffer = await ctx.markdownToImage.convertToImage(markdown) return h.image(imageBuffer, 'image/png') }) } \`\`\` ## QQ 群 - \`956758505\` `; var themePresets = { "github-light": { pageTheme: "light", codeTheme: "github", mermaidTheme: "default" }, "github-dark": { pageTheme: "dark", codeTheme: "github-dark", mermaidTheme: "dark" }, "atom-one-dark": { pageTheme: "dark", codeTheme: "atom-one-dark", mermaidTheme: "dark" }, dracula: { pageTheme: "dark", codeTheme: "dracula", mermaidTheme: "dark" }, "solarized-light": { pageTheme: "light", codeTheme: "solarized-light", mermaidTheme: "default" }, nord: { pageTheme: "dark", codeTheme: "nord", mermaidTheme: "dark" } }; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ rendering: import_koishi.Schema.object({ width: import_koishi.Schema.number().default(800).description(`截图视口的宽度。`), height: import_koishi.Schema.number().default(100).description(`截图视口的初始高度(页面内容会自动撑开)。`), deviceScaleFactor: import_koishi.Schema.number().default(2).description(`设备的缩放比率,建议为 2 以获得更清晰的图片。`), defaultImageFormat: import_koishi.Schema.union(["png", "jpeg", "webp"]).default("png").description("默认的图片输出格式。"), waitUntil: import_koishi.Schema.union([ "load", "domcontentloaded", "networkidle0", "networkidle2" ]).default("load").description( "页面加载完成的判断条件。`networkidle0` 能确保所有网络资源(如CDN上的CSS和JS)都加载完毕。" ) }).description("渲染设置") }), import_koishi.Schema.object({ theme: import_koishi.Schema.union([ // 选项一:使用预设 import_koishi.Schema.object({ mode: import_koishi.Schema.const("preset").default("preset"), preset: import_koishi.Schema.union(Object.keys(themePresets)).default("github-dark").description("选择一个开箱即用的主题预设。") }).description("预设主题"), // 选项二:使用自定义 import_koishi.Schema.object({ mode: import_koishi.Schema.const("custom"), custom: import_koishi.Schema.object({ pageTheme: import_koishi.Schema.union(["light", "dark"]).default("dark").description("整体页面的主题风格。"), codeTheme: import_koishi.Schema.string().default("github-dark").description( "代码高亮主题。请使用 [highlight.js 的主题名](https://github.com/highlightjs/highlight.js/tree/main/src/styles),例如 `github-dark`。" ), mermaidTheme: import_koishi.Schema.union(["default", "dark", "forest", "neutral"]).default("dark").description("Mermaid 图表的主题。") }) }).description("自定义主题") ]).description("主题配置").default({ mode: "preset", preset: "github-dark" }) }) ]); var MarkdownToImageService = class extends import_koishi.Service { static { __name(this, "MarkdownToImageService"); } config = {}; loggerForService; md; constructor(ctx, config) { super(ctx, "markdownToImage", true); this.config = config; this.loggerForService = ctx.logger("markdownToImage"); this.md = new import_markdown_it.default({ html: true, // 允许 HTML 标签 linkify: true, // 自动转换链接 breaks: true }).use(import_markdown_it_katex.default, { throwOnError: false, errorColor: " #cc0000" }).use(import_markdown_it_highlightjs.default, { auto: true }).use((md) => { const fence = md.renderer.rules.fence; md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx]; if (token.info.trim() === "mermaid") { return `<div class="mermaid">${token.content}</div>`; } return fence(tokens, idx, options, env, self); }; }); } getThemeSettings() { const { theme } = this.config; if (theme.mode === "preset") { return themePresets[theme.preset] || themePresets["github-dark"]; } return theme.custom; } buildHtml(body) { const { pageTheme, codeTheme, mermaidTheme } = this.getThemeSettings(); return ` <!DOCTYPE html> <html lang="en" data-color-mode="${pageTheme}" data-light-theme="light" data-dark-theme="dark"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Markdown Render</title> <!-- KaTeX CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"> <!-- Highlight.js CSS (使用解析出的 codeTheme) --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/${codeTheme}.min.css"> <!-- 主题 CSS (github-markdown-css) --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown.min.css"> <style> .markdown-body { box-sizing: border-box; min-width: 200px; max-width: 980px; margin: 0 auto; padding: 45px; } </style> </head> <body class="markdown-body"> ${body} <!-- Mermaid JS --> <script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.0/dist/mermaid.min.js"></script> <script> // 初始化 Mermaid (使用解析出的 mermaidTheme) mermaid.initialize({ startOnLoad: true, theme: '${mermaidTheme}' }); </script> </body> </html> `; } async convertToImage(markdownText) { const { pageTheme } = this.getThemeSettings(); const bodyHtml = this.md.render(markdownText); const fullHtml = this.buildHtml(bodyHtml); let page; try { page = await this.ctx.puppeteer.page(); await page.setViewport({ width: this.config.rendering.width, height: this.config.rendering.height, deviceScaleFactor: this.config.rendering.deviceScaleFactor }); await page.emulateMediaFeatures([ { name: "prefers-color-scheme", value: pageTheme } ]); await page.setContent(fullHtml, { waitUntil: this.config.rendering.waitUntil }); await page.bringToFront(); const imageBuffer = await page.screenshot({ fullPage: true, type: this.config.rendering.defaultImageFormat, omitBackground: false }); return imageBuffer; } catch (error) { this.loggerForService.error("Error converting markdown to image:", error); throw error; } finally { if (page) { await page.close(); } } } }; async function apply(ctx, config) { ctx.plugin(MarkdownToImageService, config); ctx.command( "markdownToImage [markdownText:text]", "将 Markdown 文本转换为图片" ).action(async ({ session }, markdownText) => { if (!markdownText) { await session.send("请输入你要转换的 Markdown 文本内容:"); markdownText = await session.prompt(); if (!markdownText) return `输入超时。`; } try { const imageBuffer = await ctx.markdownToImage.convertToImage( markdownText ); return import_koishi.h.image( imageBuffer, `image/${config.rendering.defaultImageFormat}` ); } catch (e) { ctx.logger("markdownToImage").warn(e); return "图片生成失败,请检查日志。"; } }); ctx.command("test-md", "测试 Markdown 图片转换").action(async ({ session }) => { const markdown = ` # Hello, Koishi This is a test. - LaTeX: $E=mc^2$ - Code: \`\`\`typescript console.log('Hello, world!') \`\`\` - Mermaid: \`\`\`mermaid graph TD; A-->B; A-->C; B-->D; C-->D; \`\`\` `; const imageBuffer = await ctx.markdownToImage.convertToImage(markdown); return import_koishi.h.image(imageBuffer, "image/png"); }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, usage });