/* 文件:markdown/widget-box.ts — 小部件盒子(widget)解析与样式注入。 */ import { Tokens, MarkedExtension } from "marked"; import { Extension } from "./extension"; import { NMPSettings } from "src/settings"; import { uevent } from "src/utils"; import { wxWidget } from 'src/wechat/weixin-api'; const widgetCache = new Map(); export function cleanWidgetCache() { widgetCache.clear(); } export class WidgetBox extends Extension { mapToString(map: Map): string { if (map.size === 0) return ""; return Array.from(map.entries()) .map(([key, value]) => `${key}=${value}`) .join("&"); // 用 "&" 连接键值对,可换成其他分隔符 } calcKey(id: string, title: string, style: Map, content: string) { const styleStr = this.mapToString(style); const key = `${id}-${title}-${styleStr}-${content}`; return key; } cacheWidget(id: string, title: string, style: Map, content: string, result: string) { const key = this.calcKey(id, title, style, content); widgetCache.set(key, result); } getWidget(id: string, title: string, style: Map, content: string) { const key = this.calcKey(id, title, style, content); if (!widgetCache.has(key)) { return null; } return widgetCache.get(key); } getBoxTitle(text: string) { let start = text.indexOf(']') + 1; let end = text.indexOf('\n'); if (end === -1) end = text.length; if (start >= end) return ''; return text.slice(start, end).trim(); } getBoxId(text: string) { const regex = /\[#(.*?)\]/g; let m; if( m = regex.exec(text)) { return m[1]; } return ""; } matched(text: string) { return this.getBoxId(text) != ""; } parseStyle(text: string) { const style = text.split(':').map((s) => s.trim()); if (style.length != 2) return null; const key = style[0]; const value = style[1]; return {key, value}; } parseBox(text: string) { const lines = text.split('\n'); let style = new Map(); let content = []; let isStyle = false; for (let line of lines) { if (line === '===') { isStyle = !isStyle; continue; } if (isStyle) { const s = this.parseStyle(line); if (s) style.set(s.key, s.value); } else { content.push(line); } } const contentStr = content.join('\n'); return { style, contentStr }; } async reqContent(id: string, title: string, style: Map, content: string) { const params = JSON.stringify({ id, title, style: Object.fromEntries(style), content }); return wxWidget(NMPSettings.getInstance().authKey, params) } processColor(style: Map) { const keys = style.keys(); for (let key of keys) { if (key.includes('color')) { const value = style.get(key); if (!value) continue; if (value.startsWith('rgb') || value.startsWith('#')) { continue; } style.set(key, '#' + value); } } } async renderer(token: Tokens.Blockquote) { let boxId = this.getBoxId(token.text); if (boxId == '') { const body = this.marked.parser(token.tokens); return `
${body}
`;; } const title = this.getBoxTitle(token.text); let style = new Map(); let content = ''; const index = token.text.indexOf('\n'); if (index > 0) { const pared = this.parseBox(token.text.slice(index + 1)) style = pared.style; content = await this.marked.parse(pared.contentStr); } this.processColor(style); const cached = this.getWidget(boxId, title, style, content); if (cached) { uevent('render-widgets-cached'); return cached; } else { const reqContent = await this.reqContent(boxId, title, style, content); this.cacheWidget(boxId, title, style, content, reqContent); uevent('render-widgets'); return reqContent; } } markedExtension(): MarkedExtension { return { extensions: [{ name: 'blockquote', level: 'block', renderer: (token: Tokens.Generic) => { return token.html; }, }] } } }