koishi-plugin-cfmrmod 1.1.2 → 1.1.3
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/dist/mcmod/cards/author-card.js +472 -0
- package/dist/mcmod/cards/center-card.js +509 -0
- package/dist/mcmod/cards/info-card.js +262 -0
- package/dist/mcmod/cards/mod-card.js +758 -0
- package/dist/mcmod/cards/tutorial-card.js +531 -0
- package/dist/mcmod/cards.js +14 -0
- package/dist/mcmod/constants.js +13 -0
- package/dist/mcmod/http.js +195 -0
- package/dist/mcmod/index.js +5 -3483
- package/dist/mcmod/plugin.js +310 -0
- package/dist/mcmod/rendering.js +388 -0
- package/dist/mcmod/search.js +122 -0
- package/dist/mcmod/utils.js +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.drawModCard = drawModCard;
|
|
4
|
+
const cheerio = require('cheerio');
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
const http_1 = require("../http");
|
|
7
|
+
const rendering_1 = require("../rendering");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
// ================= 渲染:模组/整合包卡片 (macOS 风格) =================
|
|
10
|
+
async function drawModCard(url) {
|
|
11
|
+
var _a;
|
|
12
|
+
const html = await (0, http_1.fetchMcmodText)(url, { headers: (0, http_1.getHeaders)(url) });
|
|
13
|
+
const $ = cheerio.load(html);
|
|
14
|
+
// --- 1. 数据抓取 (保持原逻辑,确保稳定性) ---
|
|
15
|
+
const titleEl = $('.class-title').clone();
|
|
16
|
+
titleEl.find('.class-official-group').remove();
|
|
17
|
+
const titleHtml = titleEl.html() || '';
|
|
18
|
+
const cleanTitleStr = titleHtml.replace(/<[^>]+>/g, '\n');
|
|
19
|
+
const titleLines = cleanTitleStr.split('\n').map(s => s.trim()).filter(s => s);
|
|
20
|
+
const title = titleLines[0] || (0, utils_1.cleanText)($('.class-title').text().replace(/开源|活跃|稳定|闭源|停更|弃坑|半弃坑|Beta/g, '').trim());
|
|
21
|
+
const subTitle = titleLines.slice(1).join(' ');
|
|
22
|
+
let coverUrl = (0, utils_1.fixUrl)($('.class-cover-image img').attr('src'));
|
|
23
|
+
let iconUrl = (0, utils_1.fixUrl)($('.class-icon img').attr('src'));
|
|
24
|
+
// 如果没有封面,用图标代替;如果没有图标,尝试用封面代替
|
|
25
|
+
if (!coverUrl && iconUrl)
|
|
26
|
+
coverUrl = iconUrl;
|
|
27
|
+
if (!iconUrl && coverUrl)
|
|
28
|
+
iconUrl = coverUrl;
|
|
29
|
+
// 标签
|
|
30
|
+
const tags = [];
|
|
31
|
+
const officialTags = new Set();
|
|
32
|
+
$('.class-official-group div').each((i, el) => {
|
|
33
|
+
const txt = (0, utils_1.cleanText)($(el).text());
|
|
34
|
+
if (!txt || txt.length > 20)
|
|
35
|
+
return;
|
|
36
|
+
officialTags.add(txt);
|
|
37
|
+
let color = '#999', bg = '#eee';
|
|
38
|
+
if (txt.includes('开源') || txt.includes('活跃') || txt.includes('稳定')) {
|
|
39
|
+
color = '#2ecc71';
|
|
40
|
+
bg = '#e8f5e9';
|
|
41
|
+
}
|
|
42
|
+
else if (txt.includes('半弃坑') || txt.includes('Beta')) {
|
|
43
|
+
color = '#f39c12';
|
|
44
|
+
bg = '#fef9e7';
|
|
45
|
+
}
|
|
46
|
+
else if (txt.includes('停更') || txt.includes('闭源') || txt.includes('弃坑')) {
|
|
47
|
+
color = '#e74c3c';
|
|
48
|
+
bg = '#fce4ec';
|
|
49
|
+
}
|
|
50
|
+
tags.push({ t: txt, bg, c: color });
|
|
51
|
+
});
|
|
52
|
+
$('.class-label-list a').each((i, el) => {
|
|
53
|
+
const labelText = (0, utils_1.cleanText)($(el).text());
|
|
54
|
+
if (!labelText || officialTags.has(labelText))
|
|
55
|
+
return;
|
|
56
|
+
const cls = $(el).attr('class') || '';
|
|
57
|
+
let bg = '#e3f2fd', c = '#3498db';
|
|
58
|
+
if (cls.includes('c_1')) {
|
|
59
|
+
bg = '#e8f5e9';
|
|
60
|
+
c = '#2ecc71';
|
|
61
|
+
}
|
|
62
|
+
else if (cls.includes('c_3')) {
|
|
63
|
+
bg = '#fff3e0';
|
|
64
|
+
c = '#e67e22';
|
|
65
|
+
}
|
|
66
|
+
tags.push({ t: labelText, bg, c });
|
|
67
|
+
});
|
|
68
|
+
// 统计数据
|
|
69
|
+
let score = (0, utils_1.cleanText)($('.class-score-num').text());
|
|
70
|
+
let scoreComment = '';
|
|
71
|
+
if (!score || score === '') {
|
|
72
|
+
score = (0, utils_1.cleanText)($('.class-excount .star .up').text()) || '0.0';
|
|
73
|
+
scoreComment = (0, utils_1.cleanText)($('.class-excount .star .down').text());
|
|
74
|
+
}
|
|
75
|
+
if (!scoreComment)
|
|
76
|
+
scoreComment = '暂无评价';
|
|
77
|
+
const yIndex = (0, utils_1.cleanText)($('.class-excount .star .text').first().text().replace('昨日指数:', '').trim());
|
|
78
|
+
let viewNum = '0', fillRate = '--';
|
|
79
|
+
$('.class-excount .infos .span').each((i, el) => {
|
|
80
|
+
const t = $(el).find('.t').text();
|
|
81
|
+
const n = (0, utils_1.cleanText)($(el).find('.n').text());
|
|
82
|
+
if (t.includes('浏览'))
|
|
83
|
+
viewNum = n;
|
|
84
|
+
if (t.includes('填充'))
|
|
85
|
+
fillRate = n;
|
|
86
|
+
});
|
|
87
|
+
function getSocialNum(className) {
|
|
88
|
+
let result = '0';
|
|
89
|
+
const selectors = [
|
|
90
|
+
`.common-fuc-group li.${className} div.nums`, `.common-fuc-group li.${className} .nums`,
|
|
91
|
+
`li.${className} div.nums`, `li.${className} .nums`
|
|
92
|
+
];
|
|
93
|
+
for (const sel of selectors) {
|
|
94
|
+
const el = $(sel);
|
|
95
|
+
if (el.length > 0) {
|
|
96
|
+
const titleAttr = el.attr('title');
|
|
97
|
+
if (titleAttr && /^\d+$/.test(titleAttr.replace(/,/g, '').trim())) {
|
|
98
|
+
result = titleAttr.replace(/,/g, '').trim();
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const text = el.text().replace(/,/g, '').trim();
|
|
102
|
+
if (text && /^\d+$/.test(text)) {
|
|
103
|
+
result = text;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
const pushNum = getSocialNum('push');
|
|
111
|
+
const favNum = getSocialNum('like');
|
|
112
|
+
const subNum = getSocialNum('subscribe');
|
|
113
|
+
// 作者
|
|
114
|
+
const authors = [];
|
|
115
|
+
$('.author-list li, .author li').each((i, el) => {
|
|
116
|
+
const n = (0, utils_1.cleanText)($(el).find('.name').text());
|
|
117
|
+
const r = (0, utils_1.cleanText)($(el).find('.position').text());
|
|
118
|
+
const iurl = (0, utils_1.fixUrl)($(el).find('img').attr('src'));
|
|
119
|
+
if (n)
|
|
120
|
+
authors.push({ n, r, i: iurl });
|
|
121
|
+
});
|
|
122
|
+
// 属性
|
|
123
|
+
const props = [];
|
|
124
|
+
$('.class-meta-list li').each((i, el) => {
|
|
125
|
+
const l = (0, utils_1.cleanText)($(el).find('h4').text());
|
|
126
|
+
const v = (0, utils_1.cleanText)($(el).find('.text').text());
|
|
127
|
+
if (l && v && !l.includes('编辑') && !l.includes('推荐') && !l.includes('收录') && !l.includes('最后')) {
|
|
128
|
+
props.push({ l, v });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// 版本
|
|
132
|
+
const versions = [];
|
|
133
|
+
const mcVerRoot = $('.mcver');
|
|
134
|
+
let verGroups = mcVerRoot.find('ul ul');
|
|
135
|
+
if (verGroups.length === 0)
|
|
136
|
+
verGroups = mcVerRoot.find('ul').first();
|
|
137
|
+
const allUls = mcVerRoot.find('ul');
|
|
138
|
+
allUls.each((i, ul) => {
|
|
139
|
+
if ($(ul).find('ul').length > 0)
|
|
140
|
+
return;
|
|
141
|
+
let loader = '';
|
|
142
|
+
const vers = [];
|
|
143
|
+
$(ul).find('li').each((j, li) => {
|
|
144
|
+
const txt = (0, utils_1.cleanText)($(li).text());
|
|
145
|
+
if (txt.includes(':') || txt.includes(':'))
|
|
146
|
+
loader = txt.replace(/[::]/g, '').trim();
|
|
147
|
+
else
|
|
148
|
+
vers.push(txt);
|
|
149
|
+
});
|
|
150
|
+
if (loader && vers.length > 0)
|
|
151
|
+
versions.push({ l: loader, v: vers.join(', ') });
|
|
152
|
+
});
|
|
153
|
+
// 链接
|
|
154
|
+
const links = [];
|
|
155
|
+
$('.common-link-icon-frame a').each((i, el) => {
|
|
156
|
+
const name = $(el).attr('data-original-title') || 'Link';
|
|
157
|
+
let sn = name;
|
|
158
|
+
if (name.includes('GitHub'))
|
|
159
|
+
sn = 'GitHub';
|
|
160
|
+
else if (name.includes('CurseForge'))
|
|
161
|
+
sn = 'CurseForge';
|
|
162
|
+
else if (name.includes('Modrinth'))
|
|
163
|
+
sn = 'Modrinth';
|
|
164
|
+
else if (name.includes('百科'))
|
|
165
|
+
sn = 'Wiki';
|
|
166
|
+
links.push(sn);
|
|
167
|
+
});
|
|
168
|
+
// 简介解析
|
|
169
|
+
const descRoot = $('.common-text').first();
|
|
170
|
+
const descNodes = [];
|
|
171
|
+
const BLOCK_TAGS = new Set(['p', 'div', 'section', 'article', 'blockquote', 'ul', 'ol']);
|
|
172
|
+
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'svg']);
|
|
173
|
+
let paragraphBuffer = '';
|
|
174
|
+
let paragraphTag = 'p';
|
|
175
|
+
const normalizeText = (text) => String(text || '')
|
|
176
|
+
.replace(/\u00a0/g, ' ')
|
|
177
|
+
.replace(/[ \t\f\v]+/g, ' ')
|
|
178
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
179
|
+
.trim();
|
|
180
|
+
const pushTextNode = (text, tag = 'p') => {
|
|
181
|
+
const normalized = normalizeText(text);
|
|
182
|
+
if (!normalized)
|
|
183
|
+
return;
|
|
184
|
+
const last = descNodes[descNodes.length - 1];
|
|
185
|
+
if ((last === null || last === void 0 ? void 0 : last.type) === 't' && last.tag === tag && tag !== 'h') {
|
|
186
|
+
last.val = `${last.val}\n${normalized}`;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
descNodes.push({ type: 't', val: normalized, tag });
|
|
190
|
+
};
|
|
191
|
+
const flushParagraph = () => {
|
|
192
|
+
if (!paragraphBuffer)
|
|
193
|
+
return;
|
|
194
|
+
pushTextNode(paragraphBuffer, paragraphTag || 'p');
|
|
195
|
+
paragraphBuffer = '';
|
|
196
|
+
paragraphTag = 'p';
|
|
197
|
+
};
|
|
198
|
+
const appendText = (text, tag = 'p') => {
|
|
199
|
+
if (!text)
|
|
200
|
+
return;
|
|
201
|
+
if (paragraphBuffer && paragraphTag !== tag)
|
|
202
|
+
flushParagraph();
|
|
203
|
+
paragraphTag = tag;
|
|
204
|
+
paragraphBuffer += text;
|
|
205
|
+
};
|
|
206
|
+
function parseNode(node, depth = 0, preferredTag = 'p') {
|
|
207
|
+
if (depth > 12)
|
|
208
|
+
return;
|
|
209
|
+
if (!node)
|
|
210
|
+
return;
|
|
211
|
+
if (node.type === 'text') {
|
|
212
|
+
appendText(node.data || '', preferredTag);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (node.type !== 'tag')
|
|
216
|
+
return;
|
|
217
|
+
const tagName = String(node.name || '').toLowerCase();
|
|
218
|
+
if (!tagName || SKIP_TAGS.has(tagName))
|
|
219
|
+
return;
|
|
220
|
+
if (tagName === 'img') {
|
|
221
|
+
const src = (0, utils_1.extractImageUrl)(node);
|
|
222
|
+
const alt = normalizeText(node.attribs.alt || '');
|
|
223
|
+
const isEmojiLikeAlt = !!alt && /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F|\p{Emoji}\u200D)+$/u.test(alt);
|
|
224
|
+
const isEmojiLikeSrc = /emoji|smilies|twemoji|emot/i.test(src || '');
|
|
225
|
+
if ((isEmojiLikeAlt || isEmojiLikeSrc) && alt) {
|
|
226
|
+
appendText(alt, preferredTag);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
flushParagraph();
|
|
230
|
+
if (src && !src.includes('icon') && !src.includes('loading')) {
|
|
231
|
+
descNodes.push({ type: 'i', src: (0, utils_1.fixUrl)(src) });
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (tagName === 'br') {
|
|
236
|
+
appendText('\n', preferredTag);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) {
|
|
240
|
+
flushParagraph();
|
|
241
|
+
pushTextNode($(node).text(), 'h');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (tagName === 'li') {
|
|
245
|
+
flushParagraph();
|
|
246
|
+
appendText('', 'li');
|
|
247
|
+
if (node.children)
|
|
248
|
+
node.children.forEach(child => parseNode(child, depth + 1, 'li'));
|
|
249
|
+
const text = normalizeText(paragraphBuffer);
|
|
250
|
+
paragraphBuffer = '';
|
|
251
|
+
paragraphTag = 'p';
|
|
252
|
+
if (text)
|
|
253
|
+
descNodes.push({ type: 'li', val: text });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (tagName === 'table') {
|
|
257
|
+
flushParagraph();
|
|
258
|
+
const galleryItems = (0, utils_1.parseGalleryFromTable)($, node);
|
|
259
|
+
if (galleryItems.length) {
|
|
260
|
+
descNodes.push({ type: 'g', items: galleryItems });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const rows = [];
|
|
264
|
+
$(node).find('tr').each((_, tr) => {
|
|
265
|
+
const row = [];
|
|
266
|
+
$(tr).find('th,td').each((__, cell) => row.push(normalizeText($(cell).text())));
|
|
267
|
+
if (row.some(Boolean))
|
|
268
|
+
rows.push(row);
|
|
269
|
+
});
|
|
270
|
+
if (rows.length)
|
|
271
|
+
descNodes.push({ type: 'tb', rows });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (tagName === 'a') {
|
|
275
|
+
const text = normalizeText($(node).text());
|
|
276
|
+
const href = (0, utils_1.fixUrl)(node.attribs.href);
|
|
277
|
+
const label = text || (0, utils_1.compactUrlText)(href);
|
|
278
|
+
if (label)
|
|
279
|
+
appendText(label, preferredTag);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const isBlock = BLOCK_TAGS.has(tagName);
|
|
283
|
+
if (isBlock)
|
|
284
|
+
flushParagraph();
|
|
285
|
+
if (node.children)
|
|
286
|
+
node.children.forEach(child => parseNode(child, depth + 1, preferredTag));
|
|
287
|
+
if (isBlock)
|
|
288
|
+
flushParagraph();
|
|
289
|
+
}
|
|
290
|
+
if (descRoot.length) {
|
|
291
|
+
descRoot[0].children.forEach(child => parseNode(child, 0));
|
|
292
|
+
flushParagraph();
|
|
293
|
+
}
|
|
294
|
+
if (descNodes.length === 0) {
|
|
295
|
+
const metaDesc = $('meta[name="description"]').attr('content');
|
|
296
|
+
if (metaDesc)
|
|
297
|
+
descNodes.push({ type: 't', val: metaDesc, tag: 'p' });
|
|
298
|
+
}
|
|
299
|
+
// --- 2. 布局计算 (macOS 风格) ---
|
|
300
|
+
const width = 800;
|
|
301
|
+
const font = rendering_1.GLOBAL_FONT_FAMILY;
|
|
302
|
+
const margin = 20; // 窗口外边距
|
|
303
|
+
const winPadding = 35; // 窗口内边距
|
|
304
|
+
const contentW = width - margin * 2 - winPadding * 2;
|
|
305
|
+
// 预计算高度
|
|
306
|
+
const dummyC = (0, rendering_1.createCanvas)(100, 100);
|
|
307
|
+
const dummy = dummyC.getContext('2d');
|
|
308
|
+
dummy.font = `bold 32px "${font}"`;
|
|
309
|
+
// 头部区域 (Header)
|
|
310
|
+
let headerH = 100; // Icon(80) + padding
|
|
311
|
+
const titleLinesNum = (0, rendering_1.wrapText)(dummy, title, 0, 0, contentW - 100, 40, 10, false) / 40;
|
|
312
|
+
headerH = Math.max(headerH, 10 + titleLinesNum * 40 + (subTitle ? 25 : 0) + (authors.length ? 40 : 0));
|
|
313
|
+
// 标签区域
|
|
314
|
+
let tagsH = 0;
|
|
315
|
+
if (tags.length)
|
|
316
|
+
tagsH = 40;
|
|
317
|
+
// 封面图 (Cover)
|
|
318
|
+
let coverH = 0;
|
|
319
|
+
if (coverUrl)
|
|
320
|
+
coverH = 300; // 固定封面显示高度
|
|
321
|
+
// 统计数据 (Stats Grid)
|
|
322
|
+
// 布局:每行4个数据
|
|
323
|
+
const statsItems = [
|
|
324
|
+
{ l: '评分', v: score }, { l: '热度', v: viewNum },
|
|
325
|
+
{ l: '推荐', v: pushNum }, { l: '收藏', v: favNum },
|
|
326
|
+
{ l: '关注', v: subNum }
|
|
327
|
+
];
|
|
328
|
+
if (fillRate !== '--')
|
|
329
|
+
statsItems.push({ l: '填充率', v: fillRate });
|
|
330
|
+
if (yIndex)
|
|
331
|
+
statsItems.push({ l: '昨日指数', v: yIndex });
|
|
332
|
+
let statsH = 0;
|
|
333
|
+
if (statsItems.length) {
|
|
334
|
+
const rows = Math.ceil(statsItems.length / 4);
|
|
335
|
+
statsH = rows * 70 + (rows - 1) * 15;
|
|
336
|
+
}
|
|
337
|
+
// 属性列表 (Props)
|
|
338
|
+
let propsH = 0;
|
|
339
|
+
if (props.length) {
|
|
340
|
+
const rows = Math.ceil(props.length / 2);
|
|
341
|
+
propsH = rows * 30 + 10;
|
|
342
|
+
}
|
|
343
|
+
// 版本和链接
|
|
344
|
+
let extraH = 0;
|
|
345
|
+
if (versions.length) {
|
|
346
|
+
extraH += 30; // Title
|
|
347
|
+
versions.forEach(v => {
|
|
348
|
+
dummy.font = `14px "${font}"`;
|
|
349
|
+
const lw = dummy.measureText(v.l).width + 10;
|
|
350
|
+
const lines = (0, rendering_1.wrapText)(dummy, v.v, 0, 0, contentW - lw, 20, 500, false) / 20;
|
|
351
|
+
extraH += lines * 20 + 10;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (links.length)
|
|
355
|
+
extraH += 50;
|
|
356
|
+
// 简介 (Desc)
|
|
357
|
+
let descH = 0;
|
|
358
|
+
dummy.font = `16px "${font}"`;
|
|
359
|
+
for (const node of descNodes) {
|
|
360
|
+
if (node.type === 't') {
|
|
361
|
+
const isHeader = node.tag === 'h';
|
|
362
|
+
dummy.font = `${isHeader ? 'bold' : ''} ${isHeader ? 22 : 16}px "${font}"`;
|
|
363
|
+
const lh = isHeader ? 32 : 26;
|
|
364
|
+
const totalNodeHeight = (0, rendering_1.wrapText)(dummy, node.val, 0, 0, contentW, lh, 5000, false);
|
|
365
|
+
descH += totalNodeHeight + (isHeader ? 15 : 10);
|
|
366
|
+
}
|
|
367
|
+
else if (node.type === 'li') {
|
|
368
|
+
dummy.font = `600 16px "${font}"`;
|
|
369
|
+
const h = (0, rendering_1.wrapText)(dummy, node.val, 0, 0, Math.max(80, contentW - 24), 26, 5000, false);
|
|
370
|
+
descH += h + 10;
|
|
371
|
+
}
|
|
372
|
+
else if (node.type === 'tb') {
|
|
373
|
+
const tableH = ((_a = (0, rendering_1.measureTableLayout)(dummy, node, contentW, 22, `600 14px "${font}"`, `800 14px "${font}"`)) === null || _a === void 0 ? void 0 : _a.totalH) || 0;
|
|
374
|
+
descH += tableH + 16;
|
|
375
|
+
}
|
|
376
|
+
else if (node.type === 'g') {
|
|
377
|
+
for (const item of node.items || []) {
|
|
378
|
+
try {
|
|
379
|
+
const img = await (0, rendering_1.loadImageWithHeaders)(item.src, constants_1.BASE_URL);
|
|
380
|
+
item.imgCache = img;
|
|
381
|
+
let scale = Math.min(contentW / img.width, 1);
|
|
382
|
+
let dw = img.width * scale;
|
|
383
|
+
let dh = img.height * scale;
|
|
384
|
+
if (dh > 460) {
|
|
385
|
+
const r = 460 / dh;
|
|
386
|
+
dh = 460;
|
|
387
|
+
dw = dw * r;
|
|
388
|
+
}
|
|
389
|
+
item.dw = dw;
|
|
390
|
+
item.dh = dh;
|
|
391
|
+
const captionH = item.caption ? (0, rendering_1.wrapText)(dummy, item.caption, 0, 0, contentW, 22, 5, false) : 0;
|
|
392
|
+
descH += dh + captionH + 26;
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
item.error = true;
|
|
396
|
+
descH += 110;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else if (node.type === 'i') {
|
|
401
|
+
try {
|
|
402
|
+
const img = await (0, rendering_1.loadImageWithHeaders)(node.src, constants_1.BASE_URL);
|
|
403
|
+
node.imgCache = img; // 缓存供绘制时使用
|
|
404
|
+
const maxH = 400;
|
|
405
|
+
let r = Math.min(contentW / img.width, maxH / img.height);
|
|
406
|
+
if (r > 1)
|
|
407
|
+
r = 1;
|
|
408
|
+
const dh = img.height * r;
|
|
409
|
+
descH += dh + 20;
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
node.imgFailed = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (node.type === 'br') {
|
|
416
|
+
descH += 10;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (descH > 0)
|
|
420
|
+
descH += 50; // Title + Padding
|
|
421
|
+
// 总高度
|
|
422
|
+
let cursorY = margin + 40; // Top traffic lights area
|
|
423
|
+
const components = [
|
|
424
|
+
{ h: tagsH, gap: 10 },
|
|
425
|
+
{ h: headerH, gap: 20 },
|
|
426
|
+
{ h: coverH, gap: 25 },
|
|
427
|
+
{ h: statsH, gap: 25 },
|
|
428
|
+
{ h: propsH, gap: 25 },
|
|
429
|
+
{ h: extraH, gap: 25 },
|
|
430
|
+
{ h: descH, gap: 20 }
|
|
431
|
+
];
|
|
432
|
+
components.forEach(c => { if (c.h > 0)
|
|
433
|
+
cursorY += c.h + c.gap; });
|
|
434
|
+
const windowH = cursorY;
|
|
435
|
+
const totalH = windowH + margin * 2;
|
|
436
|
+
// --- 3. 开始绘制 ---
|
|
437
|
+
const canvas = (0, rendering_1.createCanvas)(width, totalH);
|
|
438
|
+
const ctx = canvas.getContext('2d');
|
|
439
|
+
// 背景 (Bing 壁纸)
|
|
440
|
+
try {
|
|
441
|
+
const bgUrl = 'https://bing.biturl.top/?resolution=1920&format=image&index=0&mkt=zh-CN';
|
|
442
|
+
const bgImg = await (0, rendering_1.loadImage)(bgUrl);
|
|
443
|
+
const r = Math.max(width / bgImg.width, totalH / bgImg.height);
|
|
444
|
+
ctx.drawImage(bgImg, (width - bgImg.width * r) / 2, (totalH - bgImg.height * r) / 2, bgImg.width * r, bgImg.height * r);
|
|
445
|
+
ctx.fillStyle = 'rgba(0,0,0,0.15)'; // 遮罩
|
|
446
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
const grad = ctx.createLinearGradient(0, 0, 0, totalH);
|
|
450
|
+
grad.addColorStop(0, '#e0c3fc');
|
|
451
|
+
grad.addColorStop(1, '#8ec5fc');
|
|
452
|
+
ctx.fillStyle = grad;
|
|
453
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
454
|
+
}
|
|
455
|
+
// 窗口 (Acrylic)
|
|
456
|
+
const winX = margin;
|
|
457
|
+
const winY = margin;
|
|
458
|
+
ctx.save();
|
|
459
|
+
ctx.shadowColor = 'rgba(0,0,0,0.2)';
|
|
460
|
+
ctx.shadowBlur = 40;
|
|
461
|
+
ctx.shadowOffsetY = 20;
|
|
462
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
463
|
+
(0, rendering_1.roundRect)(ctx, winX, winY, width - margin * 2, windowH, 16);
|
|
464
|
+
ctx.fill();
|
|
465
|
+
ctx.restore();
|
|
466
|
+
// 窗口边框
|
|
467
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
|
|
468
|
+
ctx.lineWidth = 1;
|
|
469
|
+
(0, rendering_1.roundRect)(ctx, winX, winY, width - margin * 2, windowH, 16);
|
|
470
|
+
ctx.stroke();
|
|
471
|
+
// 交通灯
|
|
472
|
+
const trafficY = winY + 20;
|
|
473
|
+
['#ff5f56', '#ffbd2e', '#27c93f'].forEach((c, i) => {
|
|
474
|
+
ctx.beginPath();
|
|
475
|
+
ctx.arc(winX + 20 + i * 25, trafficY, 6, 0, Math.PI * 2);
|
|
476
|
+
ctx.fillStyle = c;
|
|
477
|
+
ctx.fill();
|
|
478
|
+
});
|
|
479
|
+
// --- 内容绘制 ---
|
|
480
|
+
let dy = winY + 50;
|
|
481
|
+
const cx = winX + winPadding;
|
|
482
|
+
// 1. Tags
|
|
483
|
+
if (tags.length) {
|
|
484
|
+
let tx = cx;
|
|
485
|
+
ctx.textBaseline = 'middle'; // Fix tag text centering
|
|
486
|
+
tags.forEach(t => {
|
|
487
|
+
ctx.font = `12px "${font}"`;
|
|
488
|
+
const tw = ctx.measureText(t.t).width + 20;
|
|
489
|
+
if (tx + tw < cx + contentW) {
|
|
490
|
+
ctx.fillStyle = t.bg;
|
|
491
|
+
(0, rendering_1.roundRect)(ctx, tx, dy, tw, 24, 6);
|
|
492
|
+
ctx.fill();
|
|
493
|
+
ctx.fillStyle = t.c;
|
|
494
|
+
ctx.fillText(t.t, tx + 10, dy + 12);
|
|
495
|
+
tx += tw + 10;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
ctx.textBaseline = 'alphabetic'; // reset
|
|
499
|
+
dy += 35;
|
|
500
|
+
}
|
|
501
|
+
// 2. Header
|
|
502
|
+
// Icon
|
|
503
|
+
const iconSize = 80;
|
|
504
|
+
if (iconUrl) {
|
|
505
|
+
try {
|
|
506
|
+
const img = await (0, rendering_1.loadImageWithHeaders)(iconUrl, constants_1.BASE_URL);
|
|
507
|
+
ctx.save();
|
|
508
|
+
(0, rendering_1.roundRect)(ctx, cx, dy, iconSize, iconSize, 12);
|
|
509
|
+
ctx.clip();
|
|
510
|
+
ctx.drawImage(img, cx, dy, iconSize, iconSize);
|
|
511
|
+
ctx.restore();
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
ctx.fillStyle = '#ddd';
|
|
515
|
+
(0, rendering_1.roundRect)(ctx, cx, dy, iconSize, iconSize, 12);
|
|
516
|
+
ctx.fill();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Title
|
|
520
|
+
const titleX = cx + iconSize + 20;
|
|
521
|
+
ctx.fillStyle = '#333';
|
|
522
|
+
ctx.font = `bold 32px "${font}"`;
|
|
523
|
+
ctx.textBaseline = 'top';
|
|
524
|
+
const titleDrawnH = (0, rendering_1.wrapText)(ctx, title, titleX, dy - 5, contentW - iconSize - 20, 40, 3, true);
|
|
525
|
+
// SubTitle
|
|
526
|
+
let subY = titleDrawnH + 5;
|
|
527
|
+
if (subTitle) {
|
|
528
|
+
ctx.fillStyle = '#888';
|
|
529
|
+
ctx.font = `16px "${font}"`;
|
|
530
|
+
ctx.fillText(subTitle, titleX, subY);
|
|
531
|
+
subY += 25;
|
|
532
|
+
}
|
|
533
|
+
// Authors
|
|
534
|
+
if (authors.length) {
|
|
535
|
+
let ax = titleX;
|
|
536
|
+
for (const a of authors.slice(0, 3)) { // 最多显示3个作者
|
|
537
|
+
ctx.save();
|
|
538
|
+
ctx.beginPath();
|
|
539
|
+
ctx.arc(ax + 12, subY + 12, 12, 0, Math.PI * 2);
|
|
540
|
+
ctx.clip();
|
|
541
|
+
if (a.i) {
|
|
542
|
+
try {
|
|
543
|
+
const img = await (0, rendering_1.loadImageWithHeaders)(a.i, constants_1.BASE_URL);
|
|
544
|
+
ctx.drawImage(img, ax, subY, 24, 24);
|
|
545
|
+
}
|
|
546
|
+
catch (e) {
|
|
547
|
+
ctx.fillStyle = '#ccc';
|
|
548
|
+
ctx.fill();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
ctx.fillStyle = '#ccc';
|
|
553
|
+
ctx.fill();
|
|
554
|
+
}
|
|
555
|
+
ctx.restore();
|
|
556
|
+
ctx.fillStyle = '#666';
|
|
557
|
+
ctx.font = `14px "${font}"`;
|
|
558
|
+
ctx.fillText(a.n, ax + 30, subY + 5);
|
|
559
|
+
ax += ctx.measureText(a.n).width + 45;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
dy += Math.max(headerH, 100) + 20;
|
|
563
|
+
// 3. Cover Image
|
|
564
|
+
if (coverUrl) {
|
|
565
|
+
try {
|
|
566
|
+
const img = await (0, rendering_1.loadImageWithHeaders)(coverUrl, constants_1.BASE_URL);
|
|
567
|
+
const coverW = contentW;
|
|
568
|
+
const coverH_Actual = 280;
|
|
569
|
+
// Crop fit
|
|
570
|
+
const r = Math.max(coverW / img.width, coverH_Actual / img.height);
|
|
571
|
+
ctx.save();
|
|
572
|
+
(0, rendering_1.roundRect)(ctx, cx, dy, coverW, coverH_Actual, 12);
|
|
573
|
+
ctx.clip();
|
|
574
|
+
ctx.drawImage(img, (coverW - img.width * r) / 2 + cx, (coverH_Actual - img.height * r) / 2 + dy, img.width * r, img.height * r);
|
|
575
|
+
ctx.restore();
|
|
576
|
+
dy += coverH_Actual + 25;
|
|
577
|
+
}
|
|
578
|
+
catch (e) { }
|
|
579
|
+
}
|
|
580
|
+
// 4. Stats Grid
|
|
581
|
+
if (statsItems.length) {
|
|
582
|
+
const cols = 4;
|
|
583
|
+
const gap = 15;
|
|
584
|
+
const itemW = (contentW - (cols - 1) * gap) / cols;
|
|
585
|
+
const itemH = 70;
|
|
586
|
+
statsItems.forEach((s, i) => {
|
|
587
|
+
const c = i % cols;
|
|
588
|
+
const r = Math.floor(i / cols);
|
|
589
|
+
const x = cx + c * (itemW + gap);
|
|
590
|
+
const y = dy + r * (itemH + gap);
|
|
591
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
592
|
+
(0, rendering_1.roundRect)(ctx, x, y, itemW, itemH, 10);
|
|
593
|
+
ctx.fill();
|
|
594
|
+
ctx.textAlign = 'center';
|
|
595
|
+
ctx.fillStyle = '#888';
|
|
596
|
+
ctx.font = `12px "${font}"`;
|
|
597
|
+
ctx.fillText(s.l, x + itemW / 2, y + 15);
|
|
598
|
+
ctx.fillStyle = '#333';
|
|
599
|
+
ctx.font = `bold 20px "${font}"`;
|
|
600
|
+
ctx.fillText(s.v, x + itemW / 2, y + 40);
|
|
601
|
+
});
|
|
602
|
+
ctx.textAlign = 'left';
|
|
603
|
+
dy += Math.ceil(statsItems.length / cols) * (itemH + gap) + 10;
|
|
604
|
+
}
|
|
605
|
+
// 5. Props List
|
|
606
|
+
if (props.length) {
|
|
607
|
+
const colW = contentW / 2;
|
|
608
|
+
props.forEach((p, i) => {
|
|
609
|
+
const c = i % 2;
|
|
610
|
+
const r = Math.floor(i / 2);
|
|
611
|
+
const x = cx + c * colW;
|
|
612
|
+
const y = dy + r * 30;
|
|
613
|
+
ctx.fillStyle = '#888';
|
|
614
|
+
ctx.font = `14px "${font}"`;
|
|
615
|
+
ctx.fillText(p.l + ':', x, y);
|
|
616
|
+
const lw = ctx.measureText(p.l + ':').width;
|
|
617
|
+
ctx.fillStyle = '#333';
|
|
618
|
+
// 截断过长文本
|
|
619
|
+
let val = p.v;
|
|
620
|
+
while (ctx.measureText(val).width > colW - lw - 20 && val.length > 5)
|
|
621
|
+
val = val.slice(0, -1);
|
|
622
|
+
if (val.length < p.v.length)
|
|
623
|
+
val += '...';
|
|
624
|
+
ctx.fillText(val, x + lw + 10, y);
|
|
625
|
+
});
|
|
626
|
+
dy += Math.ceil(props.length / 2) * 30 + 15;
|
|
627
|
+
}
|
|
628
|
+
// 6. Versions & Links
|
|
629
|
+
if (versions.length) {
|
|
630
|
+
ctx.fillStyle = '#333';
|
|
631
|
+
ctx.font = `bold 16px "${font}"`;
|
|
632
|
+
ctx.fillText('支持版本', cx, dy);
|
|
633
|
+
dy += 25;
|
|
634
|
+
versions.forEach(v => {
|
|
635
|
+
ctx.fillStyle = '#555';
|
|
636
|
+
ctx.font = `bold 14px "${font}"`;
|
|
637
|
+
ctx.fillText(v.l, cx, dy);
|
|
638
|
+
const lw = ctx.measureText(v.l).width + 10;
|
|
639
|
+
ctx.fillStyle = '#e74c3c';
|
|
640
|
+
ctx.font = `14px "${font}"`;
|
|
641
|
+
dy = (0, rendering_1.wrapText)(ctx, v.v, cx + lw, dy, contentW - lw, 20, 500, true) + 5;
|
|
642
|
+
});
|
|
643
|
+
dy += 15;
|
|
644
|
+
}
|
|
645
|
+
if (links.length) {
|
|
646
|
+
let lx = cx;
|
|
647
|
+
links.forEach(l => {
|
|
648
|
+
ctx.font = `bold 12px "${font}"`;
|
|
649
|
+
const w = ctx.measureText(l).width + 20;
|
|
650
|
+
if (lx + w < cx + contentW) {
|
|
651
|
+
ctx.fillStyle = '#333';
|
|
652
|
+
(0, rendering_1.roundRect)(ctx, lx, dy, w, 24, 12);
|
|
653
|
+
ctx.fill();
|
|
654
|
+
ctx.fillStyle = '#fff';
|
|
655
|
+
ctx.fillText(l, lx + 10, dy + 6);
|
|
656
|
+
lx += w + 10;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
dy += 45;
|
|
660
|
+
}
|
|
661
|
+
// 7. Description
|
|
662
|
+
if (descNodes.length) {
|
|
663
|
+
ctx.fillStyle = '#333';
|
|
664
|
+
ctx.font = `bold 20px "${font}"`;
|
|
665
|
+
ctx.fillText('简介', cx, dy);
|
|
666
|
+
ctx.fillStyle = '#3498db';
|
|
667
|
+
ctx.fillRect(cx, dy + 25, 40, 4);
|
|
668
|
+
dy += 45;
|
|
669
|
+
for (const node of descNodes) {
|
|
670
|
+
if (node.type === 't') {
|
|
671
|
+
const isHeader = node.tag === 'h';
|
|
672
|
+
ctx.font = `${isHeader ? '800' : '600'} ${isHeader ? 22 : 16}px "${font}"`;
|
|
673
|
+
ctx.fillStyle = isHeader ? '#2c3e50' : '#444';
|
|
674
|
+
const lh = isHeader ? 32 : 26;
|
|
675
|
+
dy = await (0, rendering_1.drawTextWithTwemoji)(ctx, node.val, cx, dy, contentW, lh, 5000, true) + (isHeader ? 15 : 10);
|
|
676
|
+
}
|
|
677
|
+
else if (node.type === 'li') {
|
|
678
|
+
const bulletX = cx + 4;
|
|
679
|
+
const textX = cx + 24;
|
|
680
|
+
ctx.fillStyle = '#444';
|
|
681
|
+
ctx.font = `600 16px "${font}"`;
|
|
682
|
+
ctx.fillText('•', bulletX, dy);
|
|
683
|
+
ctx.font = `600 16px "${font}"`;
|
|
684
|
+
dy = await (0, rendering_1.drawTextWithTwemoji)(ctx, node.val, textX, dy, Math.max(80, contentW - (textX - cx)), 26, 5000, true) + 10;
|
|
685
|
+
}
|
|
686
|
+
else if (node.type === 'tb') {
|
|
687
|
+
const tableH = (0, rendering_1.drawTable)(ctx, node, cx, dy, contentW, 22, `600 14px "${font}"`, `800 14px "${font}"`, { headerBg: 'rgba(52,152,219,0.12)', cellBg: 'rgba(255,255,255,0.7)', border: 'rgba(52,152,219,0.25)', text: '#2f3742' });
|
|
688
|
+
dy += tableH + 16;
|
|
689
|
+
}
|
|
690
|
+
else if (node.type === 'g') {
|
|
691
|
+
for (const item of node.items || []) {
|
|
692
|
+
if (item.error || !item.imgCache) {
|
|
693
|
+
ctx.fillStyle = 'rgba(0,0,0,0.06)';
|
|
694
|
+
(0, rendering_1.roundRect)(ctx, cx, dy, contentW, 90, 8);
|
|
695
|
+
ctx.fill();
|
|
696
|
+
ctx.fillStyle = '#999';
|
|
697
|
+
ctx.font = `600 14px "${font}"`;
|
|
698
|
+
ctx.fillText('Image failed to load', cx + 16, dy + 38);
|
|
699
|
+
dy += 110;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const dx = cx + (contentW - item.dw) / 2;
|
|
703
|
+
ctx.save();
|
|
704
|
+
(0, rendering_1.roundRect)(ctx, dx, dy, item.dw, item.dh, 8);
|
|
705
|
+
ctx.clip();
|
|
706
|
+
ctx.drawImage(item.imgCache, dx, dy, item.dw, item.dh);
|
|
707
|
+
ctx.restore();
|
|
708
|
+
dy += item.dh + 8;
|
|
709
|
+
if (item.caption) {
|
|
710
|
+
ctx.fillStyle = '#666';
|
|
711
|
+
ctx.font = `600 14px "${font}"`;
|
|
712
|
+
dy = await (0, rendering_1.drawTextWithTwemoji)(ctx, item.caption, cx, dy, contentW, 22, 5, true) + 12;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
dy += 8;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (node.type === 'i') {
|
|
720
|
+
if (node.imgFailed) {
|
|
721
|
+
ctx.fillStyle = 'rgba(0,0,0,0.06)';
|
|
722
|
+
(0, rendering_1.roundRect)(ctx, cx, dy, contentW, 90, 8);
|
|
723
|
+
ctx.fill();
|
|
724
|
+
ctx.fillStyle = '#999';
|
|
725
|
+
ctx.font = `600 14px "${font}"`;
|
|
726
|
+
ctx.fillText('Image failed to load', cx + 16, dy + 38);
|
|
727
|
+
dy += 110;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const img = node.imgCache || await (0, rendering_1.loadImageWithHeaders)(node.src, constants_1.BASE_URL);
|
|
732
|
+
const maxH = 400;
|
|
733
|
+
let r = Math.min(contentW / img.width, maxH / img.height);
|
|
734
|
+
if (r > 1)
|
|
735
|
+
r = 1; // 避免小图片被强制拉伸放大
|
|
736
|
+
const dw = img.width * r;
|
|
737
|
+
const dh = img.height * r;
|
|
738
|
+
ctx.save();
|
|
739
|
+
(0, rendering_1.roundRect)(ctx, cx + (contentW - dw) / 2, dy, dw, dh, 8);
|
|
740
|
+
ctx.clip();
|
|
741
|
+
ctx.drawImage(img, cx + (contentW - dw) / 2, dy, dw, dh);
|
|
742
|
+
ctx.restore();
|
|
743
|
+
dy += dh + 20;
|
|
744
|
+
}
|
|
745
|
+
catch (e) { }
|
|
746
|
+
}
|
|
747
|
+
else if (node.type === 'br') {
|
|
748
|
+
dy += 10;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Footer
|
|
753
|
+
ctx.fillStyle = '#999';
|
|
754
|
+
ctx.font = `12px "${font}"`;
|
|
755
|
+
ctx.textAlign = 'center';
|
|
756
|
+
ctx.fillText('mcmod.cn | Powered by Koishi', width / 2, totalH - 12);
|
|
757
|
+
return await canvas.encode('png');
|
|
758
|
+
}
|