mirror of
https://github.com/bruce2431/obsidian-chat-bubble-renderer.git
synced 2026-07-22 07:49:16 +00:00
- 检测 #聊天记录 标签自动识别 - 解析格式: [发送者] YYYY-MM-DD HH:MM:SS - 微信风格气泡: 对方浅灰 #f2f3f5 / 自己微信绿 #95ec69 - 引用回复灰色引用条、合并转发可展开卡片 - Obsidian ![[文件]] 内部链接转图片/音频/视频 - 全屏 fixed overlay,零触碰 Obsidian DOM - Ctrl+P 命令渲染,Esc/✕ 退出 - 基于 obsidianmd/obsidian-sample-plugin 模板
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
/**
|
|
* Chat View - 气泡对话框渲染器
|
|
* 将解析后的聊天消息渲染为微信风格气泡 UI
|
|
*/
|
|
|
|
import { parseChatLog, ChatMessage, QuoteReply, MergeForward } from './chat-parser';
|
|
|
|
export function renderChatLog(markdown: string): string {
|
|
const { preamble, messages } = parseChatLog(markdown);
|
|
|
|
let html = '';
|
|
|
|
if (preamble) {
|
|
html += `<div class="chat-preamble">${escapeHtml(preamble.replace(/\n/g, '<br>'))}</div>`;
|
|
}
|
|
|
|
if (messages.length > 0) {
|
|
html += '<div class="chat-container">';
|
|
|
|
for (const msg of messages) {
|
|
const isSelf = isSelfMessage(msg.name);
|
|
const side = isSelf ? 'self' : 'other';
|
|
const bodyHtml = renderMessageBody(msg);
|
|
|
|
html += `<div class="chat-msg ${side}">`;
|
|
html += `<div class="chat-meta">${escapeHtml(msg.name)} · ${msg.time}</div>`;
|
|
html += `<div class="chat-bubble">${bodyHtml}</div>`;
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function isSelfMessage(name: string): boolean {
|
|
const selfNames = ['bruceMTY', '我', 'me', '自己'];
|
|
return selfNames.some(n => name.toLowerCase() === n.toLowerCase());
|
|
}
|
|
|
|
function renderMessageBody(msg: ChatMessage): string {
|
|
let html = '';
|
|
for (const part of msg.body) {
|
|
if (typeof part === 'string') {
|
|
html += renderPlainText(part) + '\n';
|
|
} else if (part.type === 'quote-reply') {
|
|
html += renderQuoteReply(part);
|
|
} else if (part.type === 'merge-forward') {
|
|
html += renderMergeForward(part);
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
|
|
function renderPlainText(text: string): string {
|
|
let result = escapeHtml(text);
|
|
|
|
// Obsidian 内部链接 ![[文件名.ext|宽度]] → <img> / <audio> / <video>
|
|
result = result.replace(/!\[\[(.+?)(?:\|(\d+))?\]\]/g, (_m, file: string, w: string) => {
|
|
file = file.trim();
|
|
const ext = file.split('.').pop()?.toLowerCase() || '';
|
|
|
|
if (['mp3', 'm4a', 'wav', 'ogg', 'aac'].includes(ext)) {
|
|
return `<div class="chat-media"><audio controls src="${encodeURI(file)}" style="max-width:260px;height:32px;" onerror="this.style.display='none'"></audio></div>`;
|
|
}
|
|
if (['mp4', 'webm', 'mov'].includes(ext)) {
|
|
return `<div class="chat-media"><video controls src="${encodeURI(file)}" style="max-width:280px;max-height:200px;" onerror="this.style.display='none'"></video></div>`;
|
|
}
|
|
const width = w ? ` width="${w}"` : '';
|
|
return `<div class="chat-img"><img src="${encodeURI(file)}"${width} loading="lazy" onerror="this.style.display='none'"></div>`;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function renderQuoteReply(part: QuoteReply): string {
|
|
let html = '';
|
|
html += `<div>${renderPlainText(part.reply)}</div>`;
|
|
html += `<div class="chat-quote-bar">${escapeHtml(part.sender)}: ${renderPlainText(part.quote)}</div>`;
|
|
return html;
|
|
}
|
|
|
|
function renderMergeForward(part: MergeForward): string {
|
|
let cardHtml = '<div class="chat-forward-card">';
|
|
cardHtml += `<div class="forward-title">${escapeHtml(part.title)}</div>`;
|
|
|
|
const preview = part.items.slice(0, 3);
|
|
for (const item of preview) {
|
|
cardHtml += `<div class="forward-item">${escapeHtml(item)}</div>`;
|
|
}
|
|
if (part.items.length > 3) {
|
|
cardHtml += `<div class="forward-more">… 共 ${part.items.length} 条消息</div>`;
|
|
}
|
|
|
|
const uid = 'fw-' + Math.random().toString(36).slice(2, 8);
|
|
cardHtml += `<div class="forward-expand" onclick="document.getElementById('${uid}').classList.toggle('open')">查看全部聊天记录</div>`;
|
|
cardHtml += `<div class="forward-full" id="${uid}">`;
|
|
for (const item of part.items) {
|
|
cardHtml += `<div class="forward-item">${escapeHtml(item)}</div>`;
|
|
}
|
|
cardHtml += '</div></div>';
|
|
cardHtml += '<div class="forward-footer">聊天记录</div>';
|
|
return cardHtml;
|
|
}
|
|
|
|
function escapeHtml(str: string): string {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function encodeURI(str: string): string {
|
|
return encodeURIComponent(str).replace(/%2F/g, '/');
|
|
}
|