koishi-plugin-latex-render 1.0.10 → 1.1.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/index.js +202 -122
- package/lib/index.js +202 -122
- package/lib/renderer.d.ts +2 -7
- package/package.json +7 -6
- package/readme.md +1 -1
package/index.js
CHANGED
|
@@ -40,64 +40,182 @@ var import_koishi = require("koishi");
|
|
|
40
40
|
|
|
41
41
|
// src/renderer.ts
|
|
42
42
|
var import_katex = __toESM(require("katex"));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
__name(containsLatex, "containsLatex");
|
|
43
|
+
var import_mhchem = require("katex/contrib/mhchem");
|
|
44
|
+
var import_marked = require("marked");
|
|
47
45
|
function decodeHtmlEntities(text) {
|
|
48
46
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
49
47
|
}
|
|
50
48
|
__name(decodeHtmlEntities, "decodeHtmlEntities");
|
|
49
|
+
function autoWrapLatex(text) {
|
|
50
|
+
const lines = text.split("\n");
|
|
51
|
+
let inCodeBlock = false;
|
|
52
|
+
return lines.map((line) => {
|
|
53
|
+
if (line.trim().startsWith("```")) {
|
|
54
|
+
inCodeBlock = !inCodeBlock;
|
|
55
|
+
return line;
|
|
56
|
+
}
|
|
57
|
+
if (inCodeBlock) return line;
|
|
58
|
+
const dollarCount = (line.match(/\$/g) || []).length;
|
|
59
|
+
if (dollarCount >= 2 && dollarCount % 2 === 0 || line.includes("\\[") || line.includes("\\(") || line.includes("\\begin")) {
|
|
60
|
+
return line.replace(/\$\$(\\ce\{.+?)\$\$\}/g, (_, p1) => `$$${p1}$$`).replace(/\$(\\ce\{.+?)\$\}/g, (_, p1) => `$${p1}$`);
|
|
61
|
+
}
|
|
62
|
+
const latexPattern = /(\\[a-zA-Z]+|\^|_[0-9a-zA-Z\{])/;
|
|
63
|
+
if (!latexPattern.test(line)) return line;
|
|
64
|
+
let result = "";
|
|
65
|
+
const chunks = line.split(/([\u4e00-\u9fff\uff00-\uffef\u3000-\u303f]+)/);
|
|
66
|
+
for (let chunk of chunks) {
|
|
67
|
+
if (/[\u4e00-\u9fff\uff00-\uffef\u3000-\u303f]/.test(chunk)) {
|
|
68
|
+
result += chunk;
|
|
69
|
+
} else {
|
|
70
|
+
if (chunk.includes("http") || chunk.includes("](")) {
|
|
71
|
+
result += chunk;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const trimmed = chunk.trim();
|
|
75
|
+
if (trimmed.length > 0 && latexPattern.test(trimmed)) {
|
|
76
|
+
const isEnglishSentence = /^[a-zA-Z0-9_\s\.,!?'"-]+$/.test(trimmed) && trimmed.split(/\s+/).length > 3 && !/(\\[a-zA-Z]+|[\+\-\=\/\<\>\*])/.test(trimmed);
|
|
77
|
+
if (isEnglishSentence) {
|
|
78
|
+
result += chunk;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const leading = chunk.slice(0, chunk.indexOf(trimmed));
|
|
82
|
+
const trailing = chunk.slice(chunk.indexOf(trimmed) + trimmed.length);
|
|
83
|
+
const mathMatch = trimmed.match(/^([\s::,,。;;]*)(.*?)([\s::,,。;;]*)$/);
|
|
84
|
+
const prefix = mathMatch ? mathMatch[1] : "";
|
|
85
|
+
const coreMath = mathMatch ? mathMatch[2] : trimmed;
|
|
86
|
+
const suffix = mathMatch ? mathMatch[3] : "";
|
|
87
|
+
const listMatch = coreMath.match(/^((?:[-*+]|\d+\.)\s+)(.*)/);
|
|
88
|
+
const isAlreadyWrapped = coreMath.startsWith("$") || coreMath.startsWith("\\[") || coreMath.startsWith("\\(") || coreMath.startsWith("\\begin");
|
|
89
|
+
if (listMatch) {
|
|
90
|
+
result += `${leading}${prefix}${listMatch[1]}$${listMatch[2].replace(/^\$+|\$+$/g, "")}$${suffix}${trailing}`;
|
|
91
|
+
} else if (isAlreadyWrapped) {
|
|
92
|
+
result += `${leading}${prefix}${coreMath}${suffix}${trailing}`;
|
|
93
|
+
} else {
|
|
94
|
+
result += `${leading}${prefix}$${coreMath}$${suffix}${trailing}`;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
result += chunk;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}).join("\n");
|
|
103
|
+
}
|
|
104
|
+
__name(autoWrapLatex, "autoWrapLatex");
|
|
105
|
+
function containsMarkdown(content) {
|
|
106
|
+
return /^#{1,6}\s/m.test(content) || /\*\*[^*]+\*\*/.test(content) || /\*[^*]+\*/.test(content) || /`[^`]+`/.test(content) || /```[\s\S]+?```/.test(content) || /^\s*[-*+]\s/m.test(content) || /^\s*\d+\.\s/m.test(content) || /^\s*>\s/m.test(content) || /\[.+?\]\(.+?\)/.test(content);
|
|
107
|
+
}
|
|
108
|
+
__name(containsMarkdown, "containsMarkdown");
|
|
51
109
|
function parseMessage(content) {
|
|
52
|
-
content = decodeHtmlEntities(content);
|
|
53
110
|
const result = [];
|
|
54
|
-
const regex = /(\$\$[\s\S]+?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$
|
|
111
|
+
const regex = /(\$\$[\s\S]+?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^\$\n]+?\$|\\begin\{[a-zA-Z*]+\}[\s\S]*?\\end\{[a-zA-Z*]+\})/g;
|
|
55
112
|
let lastIndex = 0;
|
|
56
113
|
let match;
|
|
57
114
|
while ((match = regex.exec(content)) !== null) {
|
|
58
115
|
if (match.index > lastIndex) {
|
|
59
|
-
const text = content.slice(lastIndex, match.index)
|
|
60
|
-
if (text) {
|
|
61
|
-
result.push({ type: "text", content: text });
|
|
62
|
-
}
|
|
116
|
+
const text = content.slice(lastIndex, match.index);
|
|
117
|
+
if (text) result.push({ type: "text", content: text });
|
|
63
118
|
}
|
|
64
119
|
const latex = match[1];
|
|
65
120
|
let isDisplay = false;
|
|
66
|
-
let formula = latex;
|
|
67
|
-
if (
|
|
121
|
+
let formula = latex.trim();
|
|
122
|
+
if (formula.startsWith("$$") || formula.startsWith("\\[")) {
|
|
68
123
|
isDisplay = true;
|
|
69
|
-
|
|
70
|
-
} else if (latex.startsWith("\\[") && latex.endsWith("\\]")) {
|
|
71
|
-
isDisplay = true;
|
|
72
|
-
formula = latex.slice(2, -2).trim();
|
|
73
|
-
} else if (latex.startsWith("\\(") && latex.endsWith("\\)")) {
|
|
74
|
-
formula = latex.slice(2, -2).trim();
|
|
75
|
-
} else if (latex.startsWith("$") && latex.endsWith("$")) {
|
|
76
|
-
formula = latex.slice(1, -1).trim();
|
|
77
|
-
} else if (latex.startsWith("\\begin")) {
|
|
124
|
+
} else if (formula.startsWith("\\begin")) {
|
|
78
125
|
isDisplay = true;
|
|
79
126
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
display: isDisplay
|
|
84
|
-
});
|
|
127
|
+
formula = formula.replace(/^\s*\\\[|\s*\\\]$/g, "").replace(/^\s*\\\(|\s*\\\)$/g, "").replace(/^\s*\$\$|\s*\$\$$/g, "").replace(/^\s*\$|\s*\$$/g, "");
|
|
128
|
+
formula = formula.replace(/\\\\/g, "卐NEWLINE卍").replace(/(?<!\\)\$/g, "").replace(/卐NEWLINE卍/g, "\\\\").trim();
|
|
129
|
+
result.push({ type: "latex", content: formula, display: isDisplay });
|
|
85
130
|
lastIndex = match.index + match[0].length;
|
|
86
131
|
}
|
|
87
132
|
if (lastIndex < content.length) {
|
|
88
|
-
const text = content.slice(lastIndex)
|
|
89
|
-
if (text) {
|
|
90
|
-
result.push({ type: "text", content: text });
|
|
91
|
-
}
|
|
133
|
+
const text = content.slice(lastIndex);
|
|
134
|
+
if (text) result.push({ type: "text", content: text });
|
|
92
135
|
}
|
|
93
136
|
return result;
|
|
94
137
|
}
|
|
95
138
|
__name(parseMessage, "parseMessage");
|
|
96
|
-
function
|
|
139
|
+
function processChineseInLatex(formula) {
|
|
140
|
+
return formula.replace(/(?<!\\text\{)([\u4e00-\u9fff]+)(?!\})/g, "\\text{$1}").replace(/\\_/g, "_");
|
|
141
|
+
}
|
|
142
|
+
__name(processChineseInLatex, "processChineseInLatex");
|
|
143
|
+
function escapeHtml(text) {
|
|
144
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
145
|
+
}
|
|
146
|
+
__name(escapeHtml, "escapeHtml");
|
|
147
|
+
function generateHtml(content, config) {
|
|
97
148
|
const textColor = config.textColor || "#333333";
|
|
98
149
|
const bgColor = config.backgroundColor || "#ffffff";
|
|
99
150
|
const width = config.width || 800;
|
|
100
|
-
let
|
|
151
|
+
let decodedContent = decodeHtmlEntities(content);
|
|
152
|
+
const wrappedContent = autoWrapLatex(decodedContent);
|
|
153
|
+
const items = parseMessage(wrappedContent);
|
|
154
|
+
const hasMarkdown = items.some((item) => item.type === "text" && containsMarkdown(item.content));
|
|
155
|
+
let htmlContent;
|
|
156
|
+
const katexOptions = {
|
|
157
|
+
throwOnError: false,
|
|
158
|
+
// 保持不报错降级
|
|
159
|
+
strict: false
|
|
160
|
+
};
|
|
161
|
+
if (hasMarkdown) {
|
|
162
|
+
const latexCache = [];
|
|
163
|
+
let combinedMarkdown = "";
|
|
164
|
+
let latexIndex = 0;
|
|
165
|
+
for (const item of items) {
|
|
166
|
+
if (item.type === "latex") {
|
|
167
|
+
try {
|
|
168
|
+
const processedContent = processChineseInLatex(item.content);
|
|
169
|
+
const latexHtml = import_katex.default.renderToString(processedContent, { ...katexOptions, displayMode: item.display || false });
|
|
170
|
+
const placeholder = `卐LATEX${latexIndex}卍`;
|
|
171
|
+
if (item.display) {
|
|
172
|
+
latexCache.push({ placeholder, html: `<div class="latex-display">${latexHtml}</div>`, isDisplay: true });
|
|
173
|
+
combinedMarkdown += `
|
|
174
|
+
|
|
175
|
+
${placeholder}
|
|
176
|
+
|
|
177
|
+
`;
|
|
178
|
+
} else {
|
|
179
|
+
latexCache.push({ placeholder, html: `<span class="latex-inline">${latexHtml}</span>`, isDisplay: false });
|
|
180
|
+
combinedMarkdown += placeholder;
|
|
181
|
+
}
|
|
182
|
+
latexIndex++;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn(`[latex-render] 公式渲染降级: ${item.content},原因: ${e.message}`);
|
|
185
|
+
combinedMarkdown += `\`${item.content}\``;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
combinedMarkdown += item.content;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
htmlContent = import_marked.marked.parse(combinedMarkdown, { async: false });
|
|
192
|
+
for (const { placeholder, html, isDisplay } of latexCache) {
|
|
193
|
+
if (isDisplay) {
|
|
194
|
+
htmlContent = htmlContent.replace(`<p>${placeholder}</p>`, html);
|
|
195
|
+
}
|
|
196
|
+
htmlContent = htmlContent.replace(placeholder, html);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const parts = [];
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
if (item.type === "latex") {
|
|
202
|
+
try {
|
|
203
|
+
const processedContent = processChineseInLatex(item.content);
|
|
204
|
+
const latexHtml = import_katex.default.renderToString(processedContent, { ...katexOptions, displayMode: item.display || false });
|
|
205
|
+
const className = item.display ? "latex-display block" : "latex-inline";
|
|
206
|
+
parts.push(`<span class="${className}">${latexHtml}</span>`);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn(`[latex-render] 公式渲染降级: ${item.content},原因: ${e.message}`);
|
|
209
|
+
parts.push(`<code>${escapeHtml(item.content)}</code>`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
const textHtml = escapeHtml(item.content).replace(/\n/g, "<br>");
|
|
213
|
+
parts.push(`<span class="text-line">${textHtml}</span>`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
htmlContent = parts.join("");
|
|
217
|
+
}
|
|
218
|
+
return `<!DOCTYPE html>
|
|
101
219
|
<html>
|
|
102
220
|
<head>
|
|
103
221
|
<meta charset="UTF-8">
|
|
@@ -107,142 +225,104 @@ function generateHtml(parsed, config) {
|
|
|
107
225
|
body {
|
|
108
226
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
|
109
227
|
font-size: 16px;
|
|
110
|
-
line-height: 1.
|
|
228
|
+
line-height: 1.5; /* 稍微收紧基础行高 */
|
|
111
229
|
color: ${textColor};
|
|
112
230
|
background-color: ${bgColor};
|
|
113
|
-
padding: 24px;
|
|
231
|
+
padding: 24px 30px;
|
|
114
232
|
width: ${width}px;
|
|
115
|
-
min-height: 100px;
|
|
116
233
|
}
|
|
117
234
|
.content {
|
|
118
235
|
word-wrap: break-word;
|
|
119
|
-
white-space: pre-wrap;
|
|
236
|
+
/* !!!这里必须删掉之前加的 white-space: pre-wrap; !!! */
|
|
120
237
|
}
|
|
238
|
+
|
|
239
|
+
/* 严格控制基础段落间距 */
|
|
240
|
+
.content p { margin: 6px 0; }
|
|
241
|
+
.content p:empty { display: none; }
|
|
242
|
+
|
|
243
|
+
/* 🌟 核心修复:干掉 KaTeX 默认自带的巨大上下 Margin (1em) */
|
|
244
|
+
.katex-display { margin: 0 !important; }
|
|
245
|
+
|
|
246
|
+
/* 将公式容器设为 block,并接管精确的间距 */
|
|
121
247
|
.latex-display {
|
|
122
|
-
|
|
248
|
+
display: block;
|
|
249
|
+
margin: 10px 0; /* 这是真正的块级公式间距 */
|
|
123
250
|
text-align: center;
|
|
124
251
|
overflow-x: auto;
|
|
125
252
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
253
|
+
/* 兼容纯文本分支的 span */
|
|
254
|
+
.latex-display.block { display: block; }
|
|
255
|
+
|
|
256
|
+
.latex-inline { margin: 0 2px; }
|
|
257
|
+
.text-line { margin: 2px 0; }
|
|
258
|
+
|
|
259
|
+
/* 标题和列表排版优化,使其更紧凑 */
|
|
260
|
+
.content h1, .content h2, .content h3, .content h4 { margin: 16px 0 8px 0; font-weight: 600; }
|
|
261
|
+
.content ul, .content ol { margin: 6px 0; padding-left: 24px; }
|
|
262
|
+
.content li { margin: 4px 0; }
|
|
263
|
+
|
|
264
|
+
.content blockquote { margin: 8px 0; padding: 8px 16px; border-left: 4px solid #ddd; background-color: #f5f5f5; }
|
|
265
|
+
.content code { background-color: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-family: "Consolas", monospace; font-size: 0.9em; }
|
|
266
|
+
.content pre { background-color: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; }
|
|
267
|
+
.content pre code { background: none; padding: 0; }
|
|
268
|
+
.content strong { font-weight: 600; }
|
|
269
|
+
.content em { font-style: italic; }
|
|
270
|
+
.content a { color: #0066cc; text-decoration: none; }
|
|
132
271
|
</style>
|
|
133
272
|
</head>
|
|
134
273
|
<body>
|
|
135
|
-
<div class="content"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const escaped = item.content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
139
|
-
html += `<span class="text-line">${escaped}</span>`;
|
|
140
|
-
} else {
|
|
141
|
-
try {
|
|
142
|
-
const htmlContent = import_katex.default.renderToString(item.content, {
|
|
143
|
-
throwOnError: false,
|
|
144
|
-
displayMode: item.display || false,
|
|
145
|
-
trust: true,
|
|
146
|
-
strict: false
|
|
147
|
-
});
|
|
148
|
-
const className = item.display ? "latex-display" : "latex-inline";
|
|
149
|
-
html += `<span class="${className}">${htmlContent}</span>`;
|
|
150
|
-
} catch (e) {
|
|
151
|
-
html += `<span class="latex-display"><code>${item.content}</code></span>`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
html += `</div></body></html>`;
|
|
156
|
-
return html;
|
|
274
|
+
<div class="content">${htmlContent}</div>
|
|
275
|
+
</body>
|
|
276
|
+
</html>`;
|
|
157
277
|
}
|
|
158
278
|
__name(generateHtml, "generateHtml");
|
|
159
|
-
function estimateHeight(
|
|
160
|
-
|
|
161
|
-
for (const item of parsed) {
|
|
162
|
-
if (item.type === "text") {
|
|
163
|
-
charCount += item.content.length;
|
|
164
|
-
} else {
|
|
165
|
-
charCount += item.content.length * 1.5;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const lines = Math.ceil(charCount / 35);
|
|
279
|
+
function estimateHeight(content) {
|
|
280
|
+
const lines = Math.ceil(content.length / 35);
|
|
169
281
|
return Math.max(100, lines * 24 + 48);
|
|
170
282
|
}
|
|
171
283
|
__name(estimateHeight, "estimateHeight");
|
|
172
284
|
async function renderLatex(ctx, content, config) {
|
|
173
285
|
console.log("[latex-render] 开始渲染...");
|
|
174
|
-
let parsed;
|
|
175
|
-
try {
|
|
176
|
-
parsed = parseMessage(content);
|
|
177
|
-
console.log("[latex-render] 解析完成,共", parsed.length, "个片段");
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error("[latex-render] 解析消息失败:", error);
|
|
180
|
-
throw new Error(`消息解析失败: ${error}`);
|
|
181
|
-
}
|
|
182
|
-
if (parsed.length === 0) {
|
|
183
|
-
throw new Error("No content to render");
|
|
184
|
-
}
|
|
185
286
|
const width = config.width || 800;
|
|
186
|
-
const height = estimateHeight(
|
|
287
|
+
const height = estimateHeight(content);
|
|
187
288
|
let html;
|
|
188
289
|
try {
|
|
189
|
-
html = generateHtml(
|
|
190
|
-
console.log("[latex-render] HTML
|
|
290
|
+
html = generateHtml(content, config);
|
|
291
|
+
console.log("[latex-render] HTML 生成完成");
|
|
191
292
|
} catch (error) {
|
|
192
|
-
console.error("[latex-render] HTML 生成失败:", error);
|
|
193
293
|
throw new Error(`HTML 生成失败: ${error}`);
|
|
194
294
|
}
|
|
195
295
|
let page = null;
|
|
196
296
|
try {
|
|
197
297
|
page = await ctx.puppeteer.page();
|
|
198
|
-
console.log("[latex-render] Puppeteer 页面获取成功");
|
|
199
298
|
await page.setContent(html, {
|
|
200
|
-
waitUntil: "
|
|
299
|
+
waitUntil: "networkidle0",
|
|
201
300
|
timeout: 3e4
|
|
202
|
-
// 30秒超时等待 CSS
|
|
203
|
-
});
|
|
204
|
-
console.log("[latex-render] HTML 内容设置完成");
|
|
205
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
206
|
-
const actualHeight = await page.evaluate(() => {
|
|
207
|
-
const body = document.body;
|
|
208
|
-
return body ? body.scrollHeight : 0;
|
|
209
|
-
}).catch((e) => {
|
|
210
|
-
console.warn("[latex-render] 获取高度失败,使用预估高度:", e);
|
|
211
|
-
return height;
|
|
212
301
|
});
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
303
|
+
const actualHeight = await page.evaluate(() => document.body ? document.body.scrollHeight : 0);
|
|
213
304
|
const finalHeight = Math.max(actualHeight + 20, height);
|
|
214
|
-
console.log("[latex-render] 实际高度:", finalHeight);
|
|
215
305
|
const buffer = await page.screenshot({
|
|
216
|
-
clip: {
|
|
217
|
-
x: 0,
|
|
218
|
-
y: 0,
|
|
219
|
-
width,
|
|
220
|
-
height: finalHeight
|
|
221
|
-
},
|
|
306
|
+
clip: { x: 0, y: 0, width, height: finalHeight },
|
|
222
307
|
type: "png"
|
|
223
308
|
});
|
|
224
|
-
console.log("[latex-render] 截图完成,buffer 长度:", buffer.length);
|
|
225
309
|
await page.close().catch(() => {
|
|
226
310
|
});
|
|
227
311
|
page = null;
|
|
228
312
|
const base64 = Buffer.from(buffer).toString("base64");
|
|
229
|
-
|
|
230
|
-
const url = await ctx.assets.upload(dataUrl, "latex-render.png");
|
|
231
|
-
console.log("[latex-render] 图片上传完成,URL:", url);
|
|
232
|
-
return url;
|
|
313
|
+
return await ctx.assets.upload(`data:image/png;base64,${base64}`, "latex-render.png");
|
|
233
314
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (page) {
|
|
237
|
-
try {
|
|
238
|
-
await page.close();
|
|
239
|
-
} catch (e) {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
315
|
+
if (page) await page.close().catch(() => {
|
|
316
|
+
});
|
|
242
317
|
throw new Error(`LaTeX 渲染失败: ${error?.message || error}`);
|
|
243
318
|
}
|
|
244
319
|
}
|
|
245
320
|
__name(renderLatex, "renderLatex");
|
|
321
|
+
function containsLatex(content) {
|
|
322
|
+
const decoded = decodeHtmlEntities(content);
|
|
323
|
+
return /\$\$/.test(decoded) || /\\begin\{/.test(decoded) || /\\\[[\s\S]*?\\\]/.test(decoded) || /\\\([\s\S]*?\\\)/.test(decoded) || /\\[a-zA-Z]+/.test(decoded) || /\^[^{]/.test(decoded) || /_[^{]/.test(decoded) || /\$[^\$\n]/.test(decoded);
|
|
324
|
+
}
|
|
325
|
+
__name(containsLatex, "containsLatex");
|
|
246
326
|
|
|
247
327
|
// src/index.ts
|
|
248
328
|
var name = "latex-render";
|
package/lib/index.js
CHANGED
|
@@ -40,64 +40,182 @@ var import_koishi = require("koishi");
|
|
|
40
40
|
|
|
41
41
|
// src/renderer.ts
|
|
42
42
|
var import_katex = __toESM(require("katex"));
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
__name(containsLatex, "containsLatex");
|
|
43
|
+
var import_mhchem = require("katex/contrib/mhchem");
|
|
44
|
+
var import_marked = require("marked");
|
|
47
45
|
function decodeHtmlEntities(text) {
|
|
48
46
|
return text.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
49
47
|
}
|
|
50
48
|
__name(decodeHtmlEntities, "decodeHtmlEntities");
|
|
49
|
+
function autoWrapLatex(text) {
|
|
50
|
+
const lines = text.split("\n");
|
|
51
|
+
let inCodeBlock = false;
|
|
52
|
+
return lines.map((line) => {
|
|
53
|
+
if (line.trim().startsWith("```")) {
|
|
54
|
+
inCodeBlock = !inCodeBlock;
|
|
55
|
+
return line;
|
|
56
|
+
}
|
|
57
|
+
if (inCodeBlock) return line;
|
|
58
|
+
const dollarCount = (line.match(/\$/g) || []).length;
|
|
59
|
+
if (dollarCount >= 2 && dollarCount % 2 === 0 || line.includes("\\[") || line.includes("\\(") || line.includes("\\begin")) {
|
|
60
|
+
return line.replace(/\$\$(\\ce\{.+?)\$\$\}/g, (_, p1) => `$$${p1}$$`).replace(/\$(\\ce\{.+?)\$\}/g, (_, p1) => `$${p1}$`);
|
|
61
|
+
}
|
|
62
|
+
const latexPattern = /(\\[a-zA-Z]+|\^|_[0-9a-zA-Z\{])/;
|
|
63
|
+
if (!latexPattern.test(line)) return line;
|
|
64
|
+
let result = "";
|
|
65
|
+
const chunks = line.split(/([\u4e00-\u9fff\uff00-\uffef\u3000-\u303f]+)/);
|
|
66
|
+
for (let chunk of chunks) {
|
|
67
|
+
if (/[\u4e00-\u9fff\uff00-\uffef\u3000-\u303f]/.test(chunk)) {
|
|
68
|
+
result += chunk;
|
|
69
|
+
} else {
|
|
70
|
+
if (chunk.includes("http") || chunk.includes("](")) {
|
|
71
|
+
result += chunk;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const trimmed = chunk.trim();
|
|
75
|
+
if (trimmed.length > 0 && latexPattern.test(trimmed)) {
|
|
76
|
+
const isEnglishSentence = /^[a-zA-Z0-9_\s\.,!?'"-]+$/.test(trimmed) && trimmed.split(/\s+/).length > 3 && !/(\\[a-zA-Z]+|[\+\-\=\/\<\>\*])/.test(trimmed);
|
|
77
|
+
if (isEnglishSentence) {
|
|
78
|
+
result += chunk;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const leading = chunk.slice(0, chunk.indexOf(trimmed));
|
|
82
|
+
const trailing = chunk.slice(chunk.indexOf(trimmed) + trimmed.length);
|
|
83
|
+
const mathMatch = trimmed.match(/^([\s::,,。;;]*)(.*?)([\s::,,。;;]*)$/);
|
|
84
|
+
const prefix = mathMatch ? mathMatch[1] : "";
|
|
85
|
+
const coreMath = mathMatch ? mathMatch[2] : trimmed;
|
|
86
|
+
const suffix = mathMatch ? mathMatch[3] : "";
|
|
87
|
+
const listMatch = coreMath.match(/^((?:[-*+]|\d+\.)\s+)(.*)/);
|
|
88
|
+
const isAlreadyWrapped = coreMath.startsWith("$") || coreMath.startsWith("\\[") || coreMath.startsWith("\\(") || coreMath.startsWith("\\begin");
|
|
89
|
+
if (listMatch) {
|
|
90
|
+
result += `${leading}${prefix}${listMatch[1]}$${listMatch[2].replace(/^\$+|\$+$/g, "")}$${suffix}${trailing}`;
|
|
91
|
+
} else if (isAlreadyWrapped) {
|
|
92
|
+
result += `${leading}${prefix}${coreMath}${suffix}${trailing}`;
|
|
93
|
+
} else {
|
|
94
|
+
result += `${leading}${prefix}$${coreMath}$${suffix}${trailing}`;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
result += chunk;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}).join("\n");
|
|
103
|
+
}
|
|
104
|
+
__name(autoWrapLatex, "autoWrapLatex");
|
|
105
|
+
function containsMarkdown(content) {
|
|
106
|
+
return /^#{1,6}\s/m.test(content) || /\*\*[^*]+\*\*/.test(content) || /\*[^*]+\*/.test(content) || /`[^`]+`/.test(content) || /```[\s\S]+?```/.test(content) || /^\s*[-*+]\s/m.test(content) || /^\s*\d+\.\s/m.test(content) || /^\s*>\s/m.test(content) || /\[.+?\]\(.+?\)/.test(content);
|
|
107
|
+
}
|
|
108
|
+
__name(containsMarkdown, "containsMarkdown");
|
|
51
109
|
function parseMessage(content) {
|
|
52
|
-
content = decodeHtmlEntities(content);
|
|
53
110
|
const result = [];
|
|
54
|
-
const regex = /(\$\$[\s\S]+?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$
|
|
111
|
+
const regex = /(\$\$[\s\S]+?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^\$\n]+?\$|\\begin\{[a-zA-Z*]+\}[\s\S]*?\\end\{[a-zA-Z*]+\})/g;
|
|
55
112
|
let lastIndex = 0;
|
|
56
113
|
let match;
|
|
57
114
|
while ((match = regex.exec(content)) !== null) {
|
|
58
115
|
if (match.index > lastIndex) {
|
|
59
|
-
const text = content.slice(lastIndex, match.index)
|
|
60
|
-
if (text) {
|
|
61
|
-
result.push({ type: "text", content: text });
|
|
62
|
-
}
|
|
116
|
+
const text = content.slice(lastIndex, match.index);
|
|
117
|
+
if (text) result.push({ type: "text", content: text });
|
|
63
118
|
}
|
|
64
119
|
const latex = match[1];
|
|
65
120
|
let isDisplay = false;
|
|
66
|
-
let formula = latex;
|
|
67
|
-
if (
|
|
121
|
+
let formula = latex.trim();
|
|
122
|
+
if (formula.startsWith("$$") || formula.startsWith("\\[")) {
|
|
68
123
|
isDisplay = true;
|
|
69
|
-
|
|
70
|
-
} else if (latex.startsWith("\\[") && latex.endsWith("\\]")) {
|
|
71
|
-
isDisplay = true;
|
|
72
|
-
formula = latex.slice(2, -2).trim();
|
|
73
|
-
} else if (latex.startsWith("\\(") && latex.endsWith("\\)")) {
|
|
74
|
-
formula = latex.slice(2, -2).trim();
|
|
75
|
-
} else if (latex.startsWith("$") && latex.endsWith("$")) {
|
|
76
|
-
formula = latex.slice(1, -1).trim();
|
|
77
|
-
} else if (latex.startsWith("\\begin")) {
|
|
124
|
+
} else if (formula.startsWith("\\begin")) {
|
|
78
125
|
isDisplay = true;
|
|
79
126
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
display: isDisplay
|
|
84
|
-
});
|
|
127
|
+
formula = formula.replace(/^\s*\\\[|\s*\\\]$/g, "").replace(/^\s*\\\(|\s*\\\)$/g, "").replace(/^\s*\$\$|\s*\$\$$/g, "").replace(/^\s*\$|\s*\$$/g, "");
|
|
128
|
+
formula = formula.replace(/\\\\/g, "卐NEWLINE卍").replace(/(?<!\\)\$/g, "").replace(/卐NEWLINE卍/g, "\\\\").trim();
|
|
129
|
+
result.push({ type: "latex", content: formula, display: isDisplay });
|
|
85
130
|
lastIndex = match.index + match[0].length;
|
|
86
131
|
}
|
|
87
132
|
if (lastIndex < content.length) {
|
|
88
|
-
const text = content.slice(lastIndex)
|
|
89
|
-
if (text) {
|
|
90
|
-
result.push({ type: "text", content: text });
|
|
91
|
-
}
|
|
133
|
+
const text = content.slice(lastIndex);
|
|
134
|
+
if (text) result.push({ type: "text", content: text });
|
|
92
135
|
}
|
|
93
136
|
return result;
|
|
94
137
|
}
|
|
95
138
|
__name(parseMessage, "parseMessage");
|
|
96
|
-
function
|
|
139
|
+
function processChineseInLatex(formula) {
|
|
140
|
+
return formula.replace(/(?<!\\text\{)([\u4e00-\u9fff]+)(?!\})/g, "\\text{$1}").replace(/\\_/g, "_");
|
|
141
|
+
}
|
|
142
|
+
__name(processChineseInLatex, "processChineseInLatex");
|
|
143
|
+
function escapeHtml(text) {
|
|
144
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
145
|
+
}
|
|
146
|
+
__name(escapeHtml, "escapeHtml");
|
|
147
|
+
function generateHtml(content, config) {
|
|
97
148
|
const textColor = config.textColor || "#333333";
|
|
98
149
|
const bgColor = config.backgroundColor || "#ffffff";
|
|
99
150
|
const width = config.width || 800;
|
|
100
|
-
let
|
|
151
|
+
let decodedContent = decodeHtmlEntities(content);
|
|
152
|
+
const wrappedContent = autoWrapLatex(decodedContent);
|
|
153
|
+
const items = parseMessage(wrappedContent);
|
|
154
|
+
const hasMarkdown = items.some((item) => item.type === "text" && containsMarkdown(item.content));
|
|
155
|
+
let htmlContent;
|
|
156
|
+
const katexOptions = {
|
|
157
|
+
throwOnError: false,
|
|
158
|
+
// 保持不报错降级
|
|
159
|
+
strict: false
|
|
160
|
+
};
|
|
161
|
+
if (hasMarkdown) {
|
|
162
|
+
const latexCache = [];
|
|
163
|
+
let combinedMarkdown = "";
|
|
164
|
+
let latexIndex = 0;
|
|
165
|
+
for (const item of items) {
|
|
166
|
+
if (item.type === "latex") {
|
|
167
|
+
try {
|
|
168
|
+
const processedContent = processChineseInLatex(item.content);
|
|
169
|
+
const latexHtml = import_katex.default.renderToString(processedContent, { ...katexOptions, displayMode: item.display || false });
|
|
170
|
+
const placeholder = `卐LATEX${latexIndex}卍`;
|
|
171
|
+
if (item.display) {
|
|
172
|
+
latexCache.push({ placeholder, html: `<div class="latex-display">${latexHtml}</div>`, isDisplay: true });
|
|
173
|
+
combinedMarkdown += `
|
|
174
|
+
|
|
175
|
+
${placeholder}
|
|
176
|
+
|
|
177
|
+
`;
|
|
178
|
+
} else {
|
|
179
|
+
latexCache.push({ placeholder, html: `<span class="latex-inline">${latexHtml}</span>`, isDisplay: false });
|
|
180
|
+
combinedMarkdown += placeholder;
|
|
181
|
+
}
|
|
182
|
+
latexIndex++;
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn(`[latex-render] 公式渲染降级: ${item.content},原因: ${e.message}`);
|
|
185
|
+
combinedMarkdown += `\`${item.content}\``;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
combinedMarkdown += item.content;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
htmlContent = import_marked.marked.parse(combinedMarkdown, { async: false });
|
|
192
|
+
for (const { placeholder, html, isDisplay } of latexCache) {
|
|
193
|
+
if (isDisplay) {
|
|
194
|
+
htmlContent = htmlContent.replace(`<p>${placeholder}</p>`, html);
|
|
195
|
+
}
|
|
196
|
+
htmlContent = htmlContent.replace(placeholder, html);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
const parts = [];
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
if (item.type === "latex") {
|
|
202
|
+
try {
|
|
203
|
+
const processedContent = processChineseInLatex(item.content);
|
|
204
|
+
const latexHtml = import_katex.default.renderToString(processedContent, { ...katexOptions, displayMode: item.display || false });
|
|
205
|
+
const className = item.display ? "latex-display block" : "latex-inline";
|
|
206
|
+
parts.push(`<span class="${className}">${latexHtml}</span>`);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.warn(`[latex-render] 公式渲染降级: ${item.content},原因: ${e.message}`);
|
|
209
|
+
parts.push(`<code>${escapeHtml(item.content)}</code>`);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
const textHtml = escapeHtml(item.content).replace(/\n/g, "<br>");
|
|
213
|
+
parts.push(`<span class="text-line">${textHtml}</span>`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
htmlContent = parts.join("");
|
|
217
|
+
}
|
|
218
|
+
return `<!DOCTYPE html>
|
|
101
219
|
<html>
|
|
102
220
|
<head>
|
|
103
221
|
<meta charset="UTF-8">
|
|
@@ -107,142 +225,104 @@ function generateHtml(parsed, config) {
|
|
|
107
225
|
body {
|
|
108
226
|
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
|
109
227
|
font-size: 16px;
|
|
110
|
-
line-height: 1.
|
|
228
|
+
line-height: 1.5; /* 稍微收紧基础行高 */
|
|
111
229
|
color: ${textColor};
|
|
112
230
|
background-color: ${bgColor};
|
|
113
|
-
padding: 24px;
|
|
231
|
+
padding: 24px 30px;
|
|
114
232
|
width: ${width}px;
|
|
115
|
-
min-height: 100px;
|
|
116
233
|
}
|
|
117
234
|
.content {
|
|
118
235
|
word-wrap: break-word;
|
|
119
|
-
white-space: pre-wrap;
|
|
236
|
+
/* !!!这里必须删掉之前加的 white-space: pre-wrap; !!! */
|
|
120
237
|
}
|
|
238
|
+
|
|
239
|
+
/* 严格控制基础段落间距 */
|
|
240
|
+
.content p { margin: 6px 0; }
|
|
241
|
+
.content p:empty { display: none; }
|
|
242
|
+
|
|
243
|
+
/* 🌟 核心修复:干掉 KaTeX 默认自带的巨大上下 Margin (1em) */
|
|
244
|
+
.katex-display { margin: 0 !important; }
|
|
245
|
+
|
|
246
|
+
/* 将公式容器设为 block,并接管精确的间距 */
|
|
121
247
|
.latex-display {
|
|
122
|
-
|
|
248
|
+
display: block;
|
|
249
|
+
margin: 10px 0; /* 这是真正的块级公式间距 */
|
|
123
250
|
text-align: center;
|
|
124
251
|
overflow-x: auto;
|
|
125
252
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
253
|
+
/* 兼容纯文本分支的 span */
|
|
254
|
+
.latex-display.block { display: block; }
|
|
255
|
+
|
|
256
|
+
.latex-inline { margin: 0 2px; }
|
|
257
|
+
.text-line { margin: 2px 0; }
|
|
258
|
+
|
|
259
|
+
/* 标题和列表排版优化,使其更紧凑 */
|
|
260
|
+
.content h1, .content h2, .content h3, .content h4 { margin: 16px 0 8px 0; font-weight: 600; }
|
|
261
|
+
.content ul, .content ol { margin: 6px 0; padding-left: 24px; }
|
|
262
|
+
.content li { margin: 4px 0; }
|
|
263
|
+
|
|
264
|
+
.content blockquote { margin: 8px 0; padding: 8px 16px; border-left: 4px solid #ddd; background-color: #f5f5f5; }
|
|
265
|
+
.content code { background-color: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-family: "Consolas", monospace; font-size: 0.9em; }
|
|
266
|
+
.content pre { background-color: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; }
|
|
267
|
+
.content pre code { background: none; padding: 0; }
|
|
268
|
+
.content strong { font-weight: 600; }
|
|
269
|
+
.content em { font-style: italic; }
|
|
270
|
+
.content a { color: #0066cc; text-decoration: none; }
|
|
132
271
|
</style>
|
|
133
272
|
</head>
|
|
134
273
|
<body>
|
|
135
|
-
<div class="content"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const escaped = item.content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
139
|
-
html += `<span class="text-line">${escaped}</span>`;
|
|
140
|
-
} else {
|
|
141
|
-
try {
|
|
142
|
-
const htmlContent = import_katex.default.renderToString(item.content, {
|
|
143
|
-
throwOnError: false,
|
|
144
|
-
displayMode: item.display || false,
|
|
145
|
-
trust: true,
|
|
146
|
-
strict: false
|
|
147
|
-
});
|
|
148
|
-
const className = item.display ? "latex-display" : "latex-inline";
|
|
149
|
-
html += `<span class="${className}">${htmlContent}</span>`;
|
|
150
|
-
} catch (e) {
|
|
151
|
-
html += `<span class="latex-display"><code>${item.content}</code></span>`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
html += `</div></body></html>`;
|
|
156
|
-
return html;
|
|
274
|
+
<div class="content">${htmlContent}</div>
|
|
275
|
+
</body>
|
|
276
|
+
</html>`;
|
|
157
277
|
}
|
|
158
278
|
__name(generateHtml, "generateHtml");
|
|
159
|
-
function estimateHeight(
|
|
160
|
-
|
|
161
|
-
for (const item of parsed) {
|
|
162
|
-
if (item.type === "text") {
|
|
163
|
-
charCount += item.content.length;
|
|
164
|
-
} else {
|
|
165
|
-
charCount += item.content.length * 1.5;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const lines = Math.ceil(charCount / 35);
|
|
279
|
+
function estimateHeight(content) {
|
|
280
|
+
const lines = Math.ceil(content.length / 35);
|
|
169
281
|
return Math.max(100, lines * 24 + 48);
|
|
170
282
|
}
|
|
171
283
|
__name(estimateHeight, "estimateHeight");
|
|
172
284
|
async function renderLatex(ctx, content, config) {
|
|
173
285
|
console.log("[latex-render] 开始渲染...");
|
|
174
|
-
let parsed;
|
|
175
|
-
try {
|
|
176
|
-
parsed = parseMessage(content);
|
|
177
|
-
console.log("[latex-render] 解析完成,共", parsed.length, "个片段");
|
|
178
|
-
} catch (error) {
|
|
179
|
-
console.error("[latex-render] 解析消息失败:", error);
|
|
180
|
-
throw new Error(`消息解析失败: ${error}`);
|
|
181
|
-
}
|
|
182
|
-
if (parsed.length === 0) {
|
|
183
|
-
throw new Error("No content to render");
|
|
184
|
-
}
|
|
185
286
|
const width = config.width || 800;
|
|
186
|
-
const height = estimateHeight(
|
|
287
|
+
const height = estimateHeight(content);
|
|
187
288
|
let html;
|
|
188
289
|
try {
|
|
189
|
-
html = generateHtml(
|
|
190
|
-
console.log("[latex-render] HTML
|
|
290
|
+
html = generateHtml(content, config);
|
|
291
|
+
console.log("[latex-render] HTML 生成完成");
|
|
191
292
|
} catch (error) {
|
|
192
|
-
console.error("[latex-render] HTML 生成失败:", error);
|
|
193
293
|
throw new Error(`HTML 生成失败: ${error}`);
|
|
194
294
|
}
|
|
195
295
|
let page = null;
|
|
196
296
|
try {
|
|
197
297
|
page = await ctx.puppeteer.page();
|
|
198
|
-
console.log("[latex-render] Puppeteer 页面获取成功");
|
|
199
298
|
await page.setContent(html, {
|
|
200
|
-
waitUntil: "
|
|
299
|
+
waitUntil: "networkidle0",
|
|
201
300
|
timeout: 3e4
|
|
202
|
-
// 30秒超时等待 CSS
|
|
203
|
-
});
|
|
204
|
-
console.log("[latex-render] HTML 内容设置完成");
|
|
205
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
206
|
-
const actualHeight = await page.evaluate(() => {
|
|
207
|
-
const body = document.body;
|
|
208
|
-
return body ? body.scrollHeight : 0;
|
|
209
|
-
}).catch((e) => {
|
|
210
|
-
console.warn("[latex-render] 获取高度失败,使用预估高度:", e);
|
|
211
|
-
return height;
|
|
212
301
|
});
|
|
302
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
303
|
+
const actualHeight = await page.evaluate(() => document.body ? document.body.scrollHeight : 0);
|
|
213
304
|
const finalHeight = Math.max(actualHeight + 20, height);
|
|
214
|
-
console.log("[latex-render] 实际高度:", finalHeight);
|
|
215
305
|
const buffer = await page.screenshot({
|
|
216
|
-
clip: {
|
|
217
|
-
x: 0,
|
|
218
|
-
y: 0,
|
|
219
|
-
width,
|
|
220
|
-
height: finalHeight
|
|
221
|
-
},
|
|
306
|
+
clip: { x: 0, y: 0, width, height: finalHeight },
|
|
222
307
|
type: "png"
|
|
223
308
|
});
|
|
224
|
-
console.log("[latex-render] 截图完成,buffer 长度:", buffer.length);
|
|
225
309
|
await page.close().catch(() => {
|
|
226
310
|
});
|
|
227
311
|
page = null;
|
|
228
312
|
const base64 = Buffer.from(buffer).toString("base64");
|
|
229
|
-
|
|
230
|
-
const url = await ctx.assets.upload(dataUrl, "latex-render.png");
|
|
231
|
-
console.log("[latex-render] 图片上传完成,URL:", url);
|
|
232
|
-
return url;
|
|
313
|
+
return await ctx.assets.upload(`data:image/png;base64,${base64}`, "latex-render.png");
|
|
233
314
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (page) {
|
|
237
|
-
try {
|
|
238
|
-
await page.close();
|
|
239
|
-
} catch (e) {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
315
|
+
if (page) await page.close().catch(() => {
|
|
316
|
+
});
|
|
242
317
|
throw new Error(`LaTeX 渲染失败: ${error?.message || error}`);
|
|
243
318
|
}
|
|
244
319
|
}
|
|
245
320
|
__name(renderLatex, "renderLatex");
|
|
321
|
+
function containsLatex(content) {
|
|
322
|
+
const decoded = decodeHtmlEntities(content);
|
|
323
|
+
return /\$\$/.test(decoded) || /\\begin\{/.test(decoded) || /\\\[[\s\S]*?\\\]/.test(decoded) || /\\\([\s\S]*?\\\)/.test(decoded) || /\\[a-zA-Z]+/.test(decoded) || /\^[^{]/.test(decoded) || /_[^{]/.test(decoded) || /\$[^\$\n]/.test(decoded);
|
|
324
|
+
}
|
|
325
|
+
__name(containsLatex, "containsLatex");
|
|
246
326
|
|
|
247
327
|
// src/index.ts
|
|
248
328
|
var name = "latex-render";
|
package/lib/renderer.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
+
import 'katex/contrib/mhchem';
|
|
1
2
|
import { Context } from 'koishi';
|
|
2
3
|
interface Config {
|
|
3
4
|
width?: number;
|
|
4
5
|
backgroundColor?: string;
|
|
5
6
|
textColor?: string;
|
|
6
7
|
}
|
|
7
|
-
/**
|
|
8
|
-
* 检测是否为 LaTeX 公式(增强版)
|
|
9
|
-
*/
|
|
10
|
-
export declare function containsLatex(content: string): boolean;
|
|
11
|
-
/**
|
|
12
|
-
* 主渲染函数 - 使用 Puppeteer + KaTeX
|
|
13
|
-
*/
|
|
14
8
|
export declare function renderLatex(ctx: Context, content: string, config: Config): Promise<string>;
|
|
9
|
+
export declare function containsLatex(content: string): boolean;
|
|
15
10
|
export {};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-latex-render",
|
|
3
3
|
"description": "ChatLuna 专用 - 将 AI 返回的 LaTeX 公式渲染为图片",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"typings": "index.d.ts",
|
|
4
|
+
"version": "1.1.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
+
"lib",
|
|
8
9
|
"index.js",
|
|
9
|
-
"index.d.ts"
|
|
10
|
-
"lib"
|
|
10
|
+
"index.d.ts"
|
|
11
11
|
],
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"homepage": "https://github.com/gt4404gb/koishi-plugin-latex-render",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"koishi": "^4.18.7"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"katex": "^0.16.0"
|
|
31
|
+
"katex": "^0.16.0",
|
|
32
|
+
"marked": "^12.0.0"
|
|
32
33
|
},
|
|
33
34
|
"koishi": {
|
|
34
35
|
"preview": true,
|
package/readme.md
CHANGED