koishi-plugin-markdown-to-image-service
Version:
Koishi 的 Markdown 转图片服务插件。支持 LaTeX、Mermaid、代码高亮等。
343 lines (324 loc) • 11.4 kB
JavaScript
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
});