/** * Webmention 动态加载脚本 - ES6版本 * 支持三种模式:static, dynamic, hybrid * 按类型分组展示:replies, likes, reposts, bookmarks, mentions */ class WebmentionLoader { constructor() { // i18n labels (fallback to Chinese) this.labels = { title: 'Webmentions', replies: document.documentElement.lang === 'en' ? 'Replies' : '回复', likes: document.documentElement.lang === 'en' ? 'Likes' : '喜欢', reposts: document.documentElement.lang === 'en' ? 'Reposts' : '转发', bookmarks: document.documentElement.lang === 'en' ? 'Bookmarks' : '收藏', mentions: document.documentElement.lang === 'en' ? 'Mentions' : '提及', view_source: document.documentElement.lang === 'en' ? 'View source' : '查看原文', empty: document.documentElement.lang === 'en' ? 'No Webmentions yet' : '暂无 Webmentions', loading: document.documentElement.lang === 'en' ? 'Loading...' : '正在加载...', error: document.documentElement.lang === 'en' ? 'Failed to load' : '加载失败' }; this.init(); } // 内容截断函数 truncateContent(content, maxLength = 200) { if (!content) return ''; const textContent = content.replace(/<[^>]*>/g, ''); if (textContent.length <= maxLength) { return content; } const truncatedText = textContent.substring(0, maxLength).trim(); return content.includes('<') ? `

${truncatedText}…

` : `${truncatedText}…`; } // 按类型分组 mentions groupMentions(mentions) { const groups = { replies: [], likes: [], reposts: [], bookmarks: [], mentions: [] }; mentions.forEach(mention => { const prop = mention['wm-property'] || ''; if (prop === 'in-reply-to') { groups.replies.push(mention); } else if (prop === 'like-of') { groups.likes.push(mention); } else if (prop === 'repost-of') { groups.reposts.push(mention); } else if (prop === 'bookmark-of') { groups.bookmarks.push(mention); } else { groups.mentions.push(mention); } }); return groups; } // 创建卡片式 webmention HTML元素(用于 replies 和 mentions) createCardElement(mention) { const item = document.createElement('div'); item.className = 'webmention-item webmention-dynamic'; item.id = `webmention-${mention['wm-id']}`; item.setAttribute('data-webmention-id', mention['wm-id']); const authorName = mention.author ? mention.author.name || 'Anonymous' : 'Anonymous'; const authorUrl = mention.author ? mention.author.url : ''; const authorPhoto = mention.author ? mention.author.photo : ''; const authorHtml = authorUrl ? `${authorName}` : `${authorName}`; const photoHtml = authorPhoto ? `${authorName}` : ''; const publishedDate = new Date(mention.published || mention['wm-received']); const dateStr = publishedDate.toLocaleDateString('zh-CN'); let content = mention.content; if (Array.isArray(content) && content.length > 0) { content = content[0]; } const contentHtml = content ? content.html || content.text : ''; item.innerHTML = `
${photoHtml} ${authorHtml} ${dateStr}
${DOMPurify.sanitize(this.truncateContent(contentHtml))}
${this.labels.view_source}
`; return item; } // 创建紧凑式 webmention HTML元素(用于 likes, reposts, bookmarks) createCompactElement(mention) { const item = document.createElement('div'); item.className = 'webmention-compact-item'; const authorName = mention.author ? mention.author.name || 'Anonymous' : 'Anonymous'; const authorUrl = mention.author ? mention.author.url : ''; const authorPhoto = mention.author ? mention.author.photo : ''; const photoHtml = authorPhoto ? `${authorName}` : ''; const authorHtml = authorUrl ? `${authorName}` : `${authorName}`; item.innerHTML = `${photoHtml}${authorHtml}`; return item; } // 获取已存在的webmention IDs getExistingWebmentionIds(container) { const existingItems = container.querySelectorAll('[data-webmention-id]'); const ids = new Set(); existingItems.forEach(item => { const id = item.getAttribute('data-webmention-id'); if (id) { ids.add(parseInt(id, 10)); } }); return ids; } // 更新webmention计数 updateWebmentionCount(container, newCount) { const countEl = container.querySelector('.webmention-count'); if (countEl) { countEl.textContent = newCount; } } // 添加加载动画 addLoadingAnimation(element) { element.style.opacity = '0'; element.style.transform = 'translateY(10px)'; element.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; requestAnimationFrame(() => { requestAnimationFrame(() => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; }); }); } // 渲染分组内容到容器 renderGroups(container, groups, totalCount) { let html = `

${this.labels.title} (${totalCount})

`; container.innerHTML = html; // Card groups (replies, mentions) const cardGroupDefs = [ { key: 'replies', items: groups.replies }, { key: 'mentions', items: groups.mentions } ]; cardGroupDefs.forEach(({ key, items }) => { if (items.length === 0) return; const group = document.createElement('div'); group.className = `webmention-group webmention-group-${key}`; group.innerHTML = `

${this.labels[key]} (${items.length})

`; const list = document.createElement('div'); list.className = 'webmention-list'; items .sort((a, b) => new Date(a['wm-received']) - new Date(b['wm-received'])) .forEach(mention => { const element = this.createCardElement(mention); list.appendChild(element); this.addLoadingAnimation(element); }); group.appendChild(list); container.appendChild(group); }); // Compact groups (likes, reposts, bookmarks) const compactGroupDefs = [ { key: 'likes', items: groups.likes }, { key: 'reposts', items: groups.reposts }, { key: 'bookmarks', items: groups.bookmarks } ]; compactGroupDefs.forEach(({ key, items }) => { if (items.length === 0) return; const group = document.createElement('div'); group.className = `webmention-group webmention-group-${key}`; group.innerHTML = `

${this.labels[key]} (${items.length})

`; const list = document.createElement('div'); list.className = 'webmention-compact-list'; items.forEach(mention => { const element = this.createCompactElement(mention); list.appendChild(element); this.addLoadingAnimation(element); }); group.appendChild(list); container.appendChild(group); }); } // 获取webmention数据(使用target API,无需客户端过滤) async fetchWebmentions(fullUrl) { const apiUrl = `https://webmention.io/api/mentions.jf2?target=${encodeURIComponent(fullUrl)}`; try { const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); // 直接返回API结果,无需客户端处理 return data.children || []; } catch (error) { console.error('Failed to fetch webmentions:', error); throw error; } } // 静态模式:什么都不做 handleStaticMode(container) { console.log('Webmention static mode - no dynamic loading'); } // 动态模式:完全由客户端加载 async handleDynamicMode(container) { const fullUrl = container.getAttribute('data-full-url'); try { const webmentions = await this.fetchWebmentions(fullUrl); if (webmentions.length > 0) { const groups = this.groupMentions(webmentions); this.renderGroups(container, groups, webmentions.length); console.log(`Loaded ${webmentions.length} webmentions in dynamic mode`); } else { // 显示空状态 container.innerHTML = `

${this.labels.title} (0)

${this.labels.empty} `; } // 移除loading类 if (container.classList.contains('webmention-empty')) { container.classList.remove('webmention-empty'); } } catch (error) { console.error('Dynamic webmention loading failed:', error); container.innerHTML = `

${this.labels.title}

${this.labels.error} `; } } // 混合模式:与静态内容合并 async handleHybridMode(container) { const fullUrl = container.getAttribute('data-full-url'); try { const webmentions = await this.fetchWebmentions(fullUrl); // 获取已存在的webmention IDs const existingIds = this.getExistingWebmentionIds(container); // 过滤出新的webmentions const newWebmentions = webmentions.filter(mention => !existingIds.has(mention['wm-id']) ); if (newWebmentions.length > 0) { // Re-render with all webmentions grouped const allGroups = this.groupMentions(webmentions); const totalCount = existingIds.size + newWebmentions.length; this.renderGroups(container, allGroups, totalCount); if (container.classList.contains('webmention-empty')) { container.classList.remove('webmention-empty'); } console.log(`Added ${newWebmentions.length} new webmentions in hybrid mode`); } else { console.log('No new webmentions found in hybrid mode'); } } catch (error) { console.error('Hybrid webmention loading failed:', error); // 静默失败,不影响已有的静态内容 } } // 处理单个webmention容器 async processContainer(container) { const mode = container.getAttribute('data-mode') || 'static'; const fullUrl = container.getAttribute('data-full-url'); if (!fullUrl) { console.warn('Webmention container missing required data-full-url attribute'); return; } console.log(`Processing webmention container in ${mode} mode`); switch (mode) { case 'static': this.handleStaticMode(container); break; case 'dynamic': await this.handleDynamicMode(container); break; case 'hybrid': await this.handleHybridMode(container); break; default: console.warn(`Unknown webmention mode: ${mode}`); } } // 初始化 init() { const containers = document.querySelectorAll('.webmention-section[data-full-url]'); if (containers.length === 0) { return; } // 处理所有容器 containers.forEach(container => this.processContainer(container)); } } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new WebmentionLoader()); } else { new WebmentionLoader(); }