您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Silently replace page content with the parsed main article when user invokes Print. Keeps selectable text, constrains layout to A4 with 0 visual margins where possible, scales wide media to fit A4. Opens native print dialog for user confirmation. Works on most websites; excludes common search engines. MIT License. Author: iamnobody
// ==UserScript== // @name Silent Article Printer (Readability intercept) // @namespace https://example.com/iamnobody // @version 2.0.0 // @description Silently replace page content with the parsed main article when user invokes Print. Keeps selectable text, constrains layout to A4 with 0 visual margins where possible, scales wide media to fit A4. Opens native print dialog for user confirmation. Works on most websites; excludes common search engines. MIT License. Author: iamnobody // @author iamnobody // @license MIT // @match *://*/* // @exclude *://www.google.*/* // @exclude *://www.bing.com/* // @exclude *://search.yahoo.com/* // @exclude *://duckduckgo.com/* // @exclude *://www.baidu.com/* // @grant none // @run-at document-end // @require https://cdn.jsdelivr.net/npm/@mozilla/[email protected]/Readability.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js // ==/UserScript== (function () { 'use strict'; /** Behavior summary (per your choices): - Selectable text approach: when user triggers Print (Ctrl/Cmd+P or print menu), we replace page body with the sanitized article content (Readability + fallback) and let the browser's native print dialog open. - After the print action completes (or is canceled), we restore the original page so browsing continues normally. - If auto-detection fails or yields too little content, a one-time lightweight overlay asks you to click the correct article area. No persistent floating buttons. - Scale-to-fit behavior for wide tables/images (we constrain media to page width rather than switching to A3). Limitations (transparent): - Browser-controlled print headers/footers (URL, date, page numbers) cannot be programmatically disabled in all browsers. Users may need to disable them in the print dialog for a headerless PDF. - Script cannot set the filename when using the native print dialog. **/ // --- Configuration --- const MIN_ARTICLE_LENGTH = 200; // chars to consider "valid" article const OVERLAY_ID = 'iamnobody-select-overlay'; const RESTORE_TIMEOUT_MS = 2000; // safety timeout to restore page if afterprint doesn't fire // Save original state let originalHTML = null; let originalTitle = document.title; let restoring = false; let overlayActive = false; // Utility: sanitize HTML function sanitize(html) { try { return DOMPurify.sanitize(html, { ALLOWED_TAGS: false }); } catch (e) { return html; } } // Extract article via Readability with fallbacks function detectArticle() { try { const docClone = document.cloneNode(true); const parsed = new Readability(docClone).parse(); if (parsed && parsed.content && parsed.textContent && parsed.textContent.length >= MIN_ARTICLE_LENGTH) { return { title: parsed.title || document.title, content: parsed.content }; } } catch (e) { console.warn('Readability parse error', e); } // fallback selectors const selectors = ['article', 'main', '[role=main]', '.post', '.article', '#article', '.entry-content', '.content', '.post-content']; for (const sel of selectors) { const el = document.querySelector(sel); if (el && (el.innerText || '').trim().length >= MIN_ARTICLE_LENGTH) { return { title: document.title, content: el.innerHTML }; } } // fallback: largest text block const all = Array.from(document.body.querySelectorAll('p, div, section')); let best = null; let bestLen = 0; for (const el of all) { const len = (el.innerText || '').length; if (len > bestLen) { bestLen = len; best = el; } } if (best && bestLen >= MIN_ARTICLE_LENGTH) { return { title: document.title, content: best.innerHTML }; } return null; } // Build article wrapper HTML (keeps selectable text) function buildArticleDocument(title, contentHtml) { // CSS attempts to make print friendly: A4-like width and remove extra margins visually. // Note: browser print margins may still apply; user can choose "Margins: None" in print dialog when available. const css = ` html, body { height:100%; margin:0; padding:0; background: #fff; } @media screen, print { :root { --page-width:794px; } /* A4 width approx at 96dpi */ body { font-family: Georgia, 'Times New Roman', serif; color:#111; } .iamnobody-reader { box-sizing:border-box; width:var(--page-width); margin:0 auto; padding:12px; } h1.iamnobody-title { font-size:20px; margin:6px 0 10px 0; } .iamnobody-meta { font-size:12px; color:#666; margin-bottom:10px; } img { max-width:100%; height:auto; display:block; margin:8px 0; } table { max-width:100%; width:auto; border-collapse:collapse; display:block; overflow:auto; margin:8px 0; } table th, table td { border:1px solid #ccc; padding:6px 8px; } pre { white-space:pre-wrap; word-break:break-word; } /* Minimize default print margins visually */ @page { size: A4; margin: 0; } } `; const safeContent = sanitize(contentHtml); const meta = `Saved from ${location.hostname} — ${new Date().toLocaleString()}`; return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>${css}</style></head><body><article class="iamnobody-reader"><h1 class="iamnobody-title">${escapeHtml(title)}</h1><div class="iamnobody-meta">${escapeHtml(meta)}</div><div class="iamnobody-content">${safeContent}</div></article></body></html>`; } function escapeHtml(str) { if (!str) return ''; return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); } // Show small overlay to let user click the correct article area function showSelectionOverlay(onSelect) { if (overlayActive) return; overlayActive = true; const ov = document.createElement('div'); ov.id = OVERLAY_ID; Object.assign(ov.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.08)', zIndex: 2147483646, cursor: 'crosshair' }); const hint = document.createElement('div'); hint.textContent = 'Click the main article area to select it for printing. Press Esc to cancel.'; Object.assign(hint.style, { position: 'fixed', top: '12px', left: '50%', transform: 'translateX(-50%)', background:'#fff', padding:'8px 12px', borderRadius:'8px', boxShadow:'0 6px 18px rgba(0,0,0,0.12)', zIndex:2147483647 }); document.body.appendChild(ov); document.body.appendChild(hint); function clickHandler(e) { e.preventDefault(); e.stopPropagation(); // Walk up to find a meaningful container (stop at body) let el = e.target; while (el && el !== document.body) { const textLen = (el.innerText || '').trim().length; if (textLen >= MIN_ARTICLE_LENGTH) break; el = el.parentElement; } cleanup(); if (el && el !== document.body) onSelect(el); else onSelect(null); } function keyHandler(e) { if (e.key === 'Escape') { cleanup(); onSelect(null); } } function cleanup() { overlayActive = false; ov.remove(); hint.remove(); document.removeEventListener('click', clickHandler, true); document.removeEventListener('keydown', keyHandler, true); } document.addEventListener('click', clickHandler, true); document.addEventListener('keydown', keyHandler, true); } // Swap document body to article HTML and optionally change title function replaceBodyWithArticle(article) { try { if (!article || !article.content) throw new Error('No article content'); originalHTML = document.documentElement.outerHTML; originalTitle = document.title || originalTitle; const articleDoc = buildArticleDocument(article.title || originalTitle, article.content); // Use document.open/write to replace entire page so print uses the new content document.open(); document.write(articleDoc); document.close(); // Small delay to let layout settle } catch (e) { console.error('replaceBodyWithArticle failed', e); throw e; } } // Restore original page function restoreOriginal() { if (restoring) return; restoring = true; try { if (originalHTML) { // Replace document with original HTML. Use location.reload as safe fallback if writing fails. try { document.open(); document.write(originalHTML); document.close(); } catch (e) { console.warn('Restore via document.write failed, reloading page instead', e); location.reload(); } document.title = originalTitle; originalHTML = null; } } finally { restoring = false; } } // Handler when print is invoked async function handleBeforePrint(ev) { try { // If we've already replaced and are printing, no-op if (document.documentElement && document.documentElement.querySelector('.iamnobody-reader')) return; // Try auto-detect let article = detectArticle(); if (!article) { // Ask user to click selection (fallback mode) // We need to stop the print flow until selection is made. Some browsers call beforeprint synchronously. // We'll attempt to pause by showing overlay and then programmatically calling print after selection. // To avoid interfering with synchronous native print calls, we'll cancel here if overlay can't run and let print proceed normally. try { // Prevent further immediate printing by returning - let print dialog continue (best effort) // Show selection overlay; when user selects, we programmatically open a new window with article and call print there. showSelectionOverlay(function (el) { if (!el) { // user canceled selection — nothing to do return; } // Build article from chosen element const content = el.innerHTML; const title = (el.querySelector('h1') || document.querySelector('title')).innerText || document.title; const articleData = { title, content }; // Open a new window and write article then print const w = window.open('', '_blank'); if (!w) { alert('Popup blocked. Allow popups to open a print preview.'); return; } w.document.open(); w.document.write(buildArticleDocument(articleData.title, articleData.content)); w.document.close(); // Defer printing slightly to allow images to load setTimeout(()=>{ try { w.focus(); w.print(); } catch(e){ console.error(e); } }, 800); }); } catch (overlayErr) { console.warn('Selection overlay failed', overlayErr); } // Let original print continue (we couldn't reliably pause it here). Returning. return; } // We have an article. Replace body (in-place) so native print dialog prints only article. replaceBodyWithArticle(article); // Safety: restore after a timeout if afterprint doesn't fire setTimeout(()=>{ restoreOriginal(); }, RESTORE_TIMEOUT_MS + 1500); } catch (err) { console.error('beforeprint handler error', err); } } function handleAfterPrint(ev) { try { // restore original document restoreOriginal(); } catch (e) { console.error('afterprint handler error', e); } } // Attach listeners function attachPrintListeners() { try { window.addEventListener('beforeprint', handleBeforePrint); window.addEventListener('afterprint', handleAfterPrint); // Also intercept Ctrl/Cmd+P to try to handle cases where beforeprint is unreliable. window.addEventListener('keydown', function (e) { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const meta = isMac ? e.metaKey : e.ctrlKey; if (meta && e.key.toLowerCase() === 'p') { // Give browser default behavior but also try our handler (we cannot prevent default reliably in some browsers) try { handleBeforePrint(); } catch (e) { console.error(e); } // allow native dialog to open — our replacement may have already occurred } }); } catch (e) { console.error('attachPrintListeners failed', e); } } // Debugging logs? Enabled by user choice earlier. We'll keep minimal console logs but not noisy. console.log('Silent Article Printer: initialized (selectable-text mode).'); // Initialize attachPrintListeners(); })();