您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export full topics from any NodeBB forum to PDF and HTML
// ==UserScript== // @name NodeBB Print & Save // @description Export full topics from any NodeBB forum to PDF and HTML // @namespace https://www.octt.eu.org/ // @match *://*/* // @run-at context-menu // @grant GM_registerMenuCommand // @version 1.0.0 // @author OctoSpacc // @license GPL-3.0 // ==/UserScript== GM_registerMenuCommand('Open NodeBB Print & Save', async function(){ const PAGEITEMS = 20; const urlTokens = location.href.split('/topic/'); const apiUrl = (urlTokens[0] + '/api'); if (document.documentElement.innerHTML.search('/nodebb.min.js?') === -1 || urlTokens.length < 2) { alert(`Current page (${location.href}) doesn't appear to be a topic on a NodeBB site. Please open a topic page and retry.`); return; } const backgroundColor = getComputedStyle(document.body).backgroundColor; const mainContentEl = document.querySelector('main > div#content'); const overlayEl = document.body.appendChild(Object.assign(document.createElement('div'), { style: ` display: block; position: absolute; visibility: visible; z-index: 9999999; width: 100%; left: 0; top: 0; background-color: ${backgroundColor}; `, innerHTML: ` <div style=" position: sticky; top: 0; z-index: 9; padding: 1em; text-align: right; background-color: ${backgroundColor}; "> <p style="position: absolute;">${document.title}</p> <div style="position: relative; z-index: 1;"> <button name="print" onclick="print();">🖨️ Print/PDF</button> <button name="html">📄️ Download HTML</button> <button name="close">❌️ Close</button> </div> </div> <div class="topic"><ul class="${mainContentEl.querySelector('ul.posts.timeline').className}">Loading...</ul></div> ` })); const timelineEl = overlayEl.querySelector('ul.posts.timeline'); overlayEl.querySelector('button[name="html"]').onclick = () => { for (const el of document.querySelectorAll('[href]')) { el.href = el.href; } Object.assign(document.createElement('a'), { href: 'data:text/html;utf8,' + encodeURIComponent(document.doctype + document.documentElement.outerHTML), download: `${document.title}.html`, }).click(); }; overlayEl.querySelector('button[name="close"]').onclick = (event) => { event.target.parentElement.parentElement.parentElement.remove(); mainContentEl.hidden = false; mainContentEl.style = null; }; mainContentEl.hidden = true; mainContentEl.style = 'display: none !important;'; const topicUrl = ('/topic/' + urlTokens[1].split('/').slice(0, -1).join('/') + '/'); const postTemplateEl = Object.assign(document.createElement('li'), { innerHTML: mainContentEl.querySelector('ul.posts.timeline > li[component="post"]').innerHTML, }); postTemplateEl.dataset.component = 'post'; const propicStyle = postTemplateEl.querySelector('a[href] > .avatar[component="user/picture"]').getAttribute('style'); const posts = []; const postIds = {}; let postsHtml = ''; let topic = {}; for (var index=1; index<=(topic.postcount || PAGEITEMS); index+=PAGEITEMS) { const response = await fetch(apiUrl + topicUrl + index); topic = await response.json(); topic.posts.forEach(post => { if (postIds[post.pid]) { return; } posts.push(post); postIds[post.pid] = true; timelineEl.innerHTML += `<br />${post.pid}`; }); } posts.forEach(post => { postTemplateEl.querySelector('.timeago[datetime]').innerHTML = post.timestampISO; postTemplateEl.querySelector('a[href][data-username][data-uid]').innerHTML = post.user.username; postTemplateEl.querySelector('a[href] > .avatar[component="user/picture"]').parentElement.innerHTML = (post.user.picture ? `<img class="avatar" component="user/picture" style="${propicStyle}" src="${post.user.picture}"/>` : `<span class="avatar" component="user/picture" style="${propicStyle} background-color: ${post.user['icon:bgColor']};">${post.user['icon:text']}</span>` ); postTemplateEl.querySelector('div.content[component="post/content"]').innerHTML = post.content; postTemplateEl.querySelector('[component="post/vote-count"][data-votes]').innerHTML = (post.upvotes - post.downvotes); postsHtml += postTemplateEl.outerHTML; }); timelineEl.innerHTML = postsHtml; });
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址