您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save your DBB books to PDF!
/* globals waitForKeyElements */ // ==UserScript== // @name DDB Book Downloader // @namespace http://tampermonkey.net/ // @version 0.1.9 // @description Save your DBB books to PDF! // @author rsminsmith (Adapted from C T Zaran, print styling from /u/Ninjaen37) // @match https://www.dndbeyond.com/sources/* // @icon https://www.google.com/s2/favicons?sz=64&domain=dndbeyond.com // @grant none // ==/UserScript== (function() { 'use strict'; const minPageDelay = 0; // Time to wait between each page. Increase if you're getting bot detected const maxPageDelay = 0; let bookHTML = ''; // Get necessary URL components const slug = document.location.pathname; const urlParts = slug.split('/'); if (urlParts[1] != 'sources') { console.info('DNDBeyondPdf: Not a source page'); return; } // If this is a subpage, only worry about indexing it const tocHeaderEl = document.querySelector('.compendium-toc-full-header'); if (!tocHeaderEl) { console.info('DNDBeyondPdf: Not a ToC page, ignoring'); return; } // Build helper container and button elements const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'inline-block'; buttonContainer.style.float = 'right'; // Build buttons const resetButton = document.createElement('button'); resetButton.classList.add('reset-pdf'); resetButton.textContent = 'Reset Cache'; resetButton.type = 'button'; const pdfButton = document.createElement('button'); pdfButton.classList.add('generate-pdf'); pdfButton.textContent = 'Generate PDF'; pdfButton.type = 'button'; // Spelljammer has 3 books on one page; need to export them individually if (slug === '/sources/sais') { buttonContainer.style.backgroundColor = 'yellow'; buttonContainer.style.color = 'black'; buttonContainer.style.padding = '0.5em'; buttonContainer.textContent = 'Navigate to each book within Spelljammer to generate PDF'; } else { // Otherwise append generate and reset buttons buttonContainer.append(pdfButton); buttonContainer.append(resetButton); } // Attach buttons to header tocHeaderEl.prepend(buttonContainer); // Handle PDF generation pdfButton.onclick = async function() { // Update the button pdfButton.textContent = 'Generating...'; pdfButton.disabled = true; // Only fetch data if needed if (!bookHTML) { // Get book info const bookTitle = document.title; // Pull all links in ToC and filter out the ones that aren't needed let tocLinkEls = document.querySelectorAll('.compendium-toc-full-text a'); let validPages = []; let pageNames = {}; for (const a of tocLinkEls) { let href = a.href.replace(/#.*$/, ''); // remove the fragment from the url if any. if (validPages.includes(href)) continue; // skip duplicates validPages.push(href); pageNames[href] = a.textContent; } // Create initial page HTML bookHTML = '<!DOCTYPE html>' + '<html lang="en-us" class="no-js">'+ '<head>' + '<meta charset="UTF-8">' + '<title>' + bookTitle + '</title>' + '<link rel="stylesheet" href="https://www.dndbeyond.com/content/1-0-2352-0/skins/blocks/css/compiled.css"/>' + '<link rel="stylesheet" href="https://www.dndbeyond.com/content/1-0-2352-0/skins/waterdeep/css/compiled.css"/>' + '<link rel="stylesheet" type="text/css" href="https://www.dndbeyond.com/api/custom-css" />' + '<style>body {width: 850px; margin-left:30px} table{text-align: left;} figcaption{text-align: center;} img {max-width: 100%;} ' + '@media print {h1, h2.compendium-hr {break-before: always; page-break-before: always;} ' + '.print-section {break-after: always; page-break-after: always; } ' + 'h2.heading-anchor, caption {break-before: avoid; page-break-before: avoid} h1, h2, h3 {break-after: avoid; page-break-after: avoid;} ' + 'aside, blockquote, table, ul, ol, figure, img {break-inside: avoid; page-break-inside: avoid;} ' + '.compendium-image-left {float: left; display: block;} .compendium-image-right {float: right; display: block;} ' + '.monster-image-left {float: left; display: block;} .monster-image-right {float: right; display: block;} ' + 'img.compendium-center-banner-img {width: 100%;}}</style>' + '</head>' + '<body>' + '<div class="print-section"> '; // Replace ToC links with internal links const baseUrl = 'https://www.dndbeyond.com/sources/'; let tocEl = document.querySelector('.compendium-toc-full-text').cloneNode(true); tocEl.querySelectorAll('a[href]').forEach((tocAEl) => { // Get the link, remove the irrelevant pieces and any anchor tags let sectionName = tocAEl.href.replace(baseUrl, ''); const anchorPos = sectionName.indexOf('#'); if (anchorPos >= 0) { sectionName = sectionName.substring(0, anchorPos); } const slugPieces = sectionName.split('/'); const pageName = slugPieces[slugPieces.length - 1]; const isImage = sectionName.indexOf('.jpg') >= 0 || sectionName.indexOf('.png') >= 0; // Update the link to point to the generated internal section if (isImage) { const imageTitle = pageNames[sectionName]; const imageName = imageTitle.toLowerCase().replace(' ', '_'); tocAEl.href = '#section-' + imageName; } else { tocAEl.href = '#section-' + pageName; } }); // Finish ToC bookHTML += tocEl.outerHTML + '</div>'; // Loop through each page with a delay and store the contents by slug const numPages = validPages.length; let currentPage = 0; for (const page of validPages) { // Update button currentPage += 1; pdfButton.textContent = `Generating (${currentPage}/${numPages})...`; // Get page info const slugPieces = page.split('/'); const pageName = slugPieces[slugPieces.length - 1]; const isImage = pageName.indexOf('.jpg') >= 0 || pageName.indexOf('.png') >= 0; // Handle image links if (isImage) { console.info('DNDBeyondPdf: Including image ', page); const imageTitle = pageNames[page]; const sectionName = imageTitle.toLowerCase().replace(' ', '_'); bookHTML += '<div id="section-' + sectionName +'" class="print-section">' + '<h1>' + imageTitle + '</h1>' + '<img src="' + page + '" alt="' + imageTitle + '" class="ddb-lightbox-inner" />' + '</div>'; } else { // Handle actual content pages console.info('DNDBeyondPdf: Fetching page ', slugPieces[slugPieces.length - 1]); const pageRequest = await fetch(page); const pageContent = await pageRequest.text(); let tempEl = document.createElement('div'); tempEl.innerHTML = pageContent; bookHTML += '<div id="section-' + pageName +'" class="print-section"> ' + tempEl.querySelector('.p-article-content').innerHTML + '</div>';; } // Wait before requesting next section if required const delay = minPageDelay + (Math.random() * (maxPageDelay - minPageDelay)); await new Promise(r => setTimeout(r, delay)); } // Finalize page bookHTML += '</body></html>'; } // Open book HTML in a new tab const openBook = window.open(); openBook.document.write(bookHTML); // Re-enable the button pdfButton.textContent = 'Generate PDF'; pdfButton.disabled = false; // Log completion console.info('DNDBeyondPdf: Done!'); } // Handle cache reset resetButton.onclick = function() { bookHTML = ''; // Update the button resetButton.textContent = 'Done!'; resetButton.disabled = true; // Re-enable the button setTimeout(function() { resetButton.textContent = 'Reset Cache'; resetButton.disabled = false; }, 2500) } })();