NodeBB Print & Save

Export full topics from any NodeBB forum to PDF and HTML

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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;

});