Greasy Fork is available in English.
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;});