您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add support for quotes, bold, underline, code blocks, and bullet points on Hacker News
// ==UserScript== // @name HN Markdown Formatter // @namespace http://tampermonkey.net/ // @version 1.1 // @description Add support for quotes, bold, underline, code blocks, and bullet points on Hacker News // @author sacrosaunt // @match http://news.ycombinator.com/* // @match https://news.ycombinator.com/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; // Inject CSS styles const css = ` /* HN Text Formatter Styles */ /* Bold text */ .commtext strong, .toptext strong { font-weight: bold; } /* Italic text */ .commtext em, .toptext em { font-style: italic; } /* Underlined text */ .commtext u, .toptext u { text-decoration: underline; } /* Code blocks */ .commtext pre, .toptext pre { background-color: #f6f6f6; border: 1px solid #ddd; border-radius: 3px; padding: 8px 12px; margin: 8px 0; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 12px; line-height: 1.4; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; } /* Inline code */ .commtext code, .toptext code { background-color: #f6f6f6; border: 1px solid #ddd; border-radius: 2px; padding: 1px 4px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 12px; } /* Don't double-style code inside pre blocks */ .commtext pre code, .toptext pre code { background: none; border: none; padding: 0; } /* Blockquotes */ .commtext blockquote, .toptext blockquote { border-left: 4px solid #ccc; margin: 8px 0; padding: 4px 0 4px 12px; color: #666; font-style: italic; } /* Bullet points */ .commtext ul, .toptext ul { margin: 8px 0; padding-left: 20px; } .commtext ul li, .toptext ul li { list-style-type: disc; margin: 2px 0; } /* Nested lists */ .commtext ul ul, .toptext ul ul { margin: 2px 0; padding-left: 16px; } .commtext ul ul li, .toptext ul ul li { list-style-type: circle; } /* Ordered lists */ .commtext ol, .toptext ol { margin: 8px 0; padding-left: 20px; } .commtext ol li, .toptext ol li { list-style-type: decimal; margin: 2px 0; } /* Ensure proper spacing for formatted elements */ .commtext p, .toptext p { margin: 8px 0; } .commtext p:first-child, .toptext p:first-child { margin-top: 0; } .commtext p:last-child, .toptext p:last-child { margin-bottom: 0; } `; // Create and inject style element const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); // Function to parse and format text with markdown-like syntax function formatText(text) { // Process blockquotes BEFORE escaping HTML (since we need to match >) text = text.replace(/^>\s*(.+)$/gm, '<blockquote>$1</blockquote>'); // Escape HTML to prevent XSS (but preserve our blockquote tags) text = text.replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') // Restore blockquote tags .replace(/<blockquote>/g, '<blockquote>') .replace(/<\/blockquote>/g, '</blockquote>'); // Code blocks (triple backticks) text = text.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); // Inline code (single backticks) text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>'); // Bold text (**text** or __text__) text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); text = text.replace(/__(.*?)__/g, '<strong>$1</strong>'); // Italic text (*text* or _text_) text = text.replace(/\*([^*\n]+)\*/g, '<em>$1</em>'); text = text.replace(/_([^_\n]+)_/g, '<em>$1</em>'); // Underlined text (~text~) text = text.replace(/~([^~\n]+)~/g, '<u>$1</u>'); // Bullet points (- item or * item) const lines = text.split('\n'); let inList = false; let formattedLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const bulletMatch = line.match(/^(\s*)[-*]\s+(.+)$/); if (bulletMatch) { const indent = bulletMatch[1].length; const content = bulletMatch[2]; if (!inList) { formattedLines.push('<ul>'); inList = true; } formattedLines.push(`<li>${content}</li>`); } else { if (inList && line.trim() === '') { // Empty line in list - continue list formattedLines.push(''); } else if (inList) { // Non-bullet line - end list formattedLines.push('</ul>'); inList = false; formattedLines.push(line); } else { formattedLines.push(line); } } } // Close any open list if (inList) { formattedLines.push('</ul>'); } text = formattedLines.join('\n'); // Convert line breaks to paragraphs text = text.replace(/\n\n+/g, '</p><p>'); text = '<p>' + text + '</p>'; // Clean up empty paragraphs text = text.replace(/<p><\/p>/g, ''); text = text.replace(/<p>\s*<\/p>/g, ''); return text; } // Function to decode HTML entities function decodeHtmlEntities(text) { const textarea = document.createElement('textarea'); textarea.innerHTML = text; return textarea.value; } // Function to process entire comment element function processElement(element) { // Skip if already processed if (element.hasAttribute('data-hn-processed')) { return; } // Get the raw HTML content let htmlContent = element.innerHTML; // Store links temporarily to preserve them const links = []; let linkIndex = 0; htmlContent = htmlContent.replace(/<a[^>]*>.*?<\/a>/g, function(match) { const placeholder = `HNFORMATTERLINK${linkIndex}HNFORMATTERLINK`; links[linkIndex] = match; linkIndex++; return placeholder; }); // Decode HTML entities to get back the original text let textContent = decodeHtmlEntities(htmlContent); // Remove any existing HN formatting tags textContent = textContent.replace(/<\/?i>/g, ''); textContent = textContent.replace(/<\/?b>/g, ''); textContent = textContent.replace(/<\/?em>/g, ''); textContent = textContent.replace(/<\/?strong>/g, ''); // Preserve paragraph breaks textContent = textContent.replace(/<p>/g, '\n\n'); textContent = textContent.replace(/<\/p>/g, ''); textContent = textContent.replace(/<br\s*\/?>/g, '\n'); // Clean up extra whitespace textContent = textContent.replace(/^\s+|\s+$/g, ''); textContent = textContent.replace(/\n\s*\n\s*\n/g, '\n\n'); // Check if text contains formatting syntax const hasFormatting = /(\*\*.*?\*\*|__.*?__|`.*?`|```[\s\S]*?```|\*[^*\n]+\*|_[^_\n]+_|~[^~\n]+~|^>\s*.+$|^\s*[-*]\s+.+$)/m.test(textContent); if (hasFormatting) { let formattedHTML = formatText(textContent); // Restore links for (let i = 0; i < links.length; i++) { formattedHTML = formattedHTML.replace(`HNFORMATTERLINK${i}HNFORMATTERLINK`, links[i]); } element.innerHTML = formattedHTML; element.setAttribute('data-hn-formatted', 'true'); } else { // Even if no formatting, restore links for (let i = 0; i < links.length; i++) { textContent = textContent.replace(`HNFORMATTERLINK${i}HNFORMATTERLINK`, links[i]); } if (links.length > 0) { element.innerHTML = textContent; } } } // Function to process comments and posts function processHNContent() { // Process comment text const commentElements = document.querySelectorAll('.commtext:not([data-hn-processed])'); commentElements.forEach(element => { processElement(element); element.setAttribute('data-hn-processed', 'true'); }); // Process post text (story text) const postElements = document.querySelectorAll('.toptext:not([data-hn-processed])'); postElements.forEach(element => { processElement(element); element.setAttribute('data-hn-processed', 'true'); }); } // Initial processing processHNContent(); // Watch for dynamically loaded content const observer = new MutationObserver(function(mutations) { let shouldProcess = false; mutations.forEach(function(mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function(node) { if (node.nodeType === Node.ELEMENT_NODE) { // Check if new comments or posts were added if (node.querySelector && (node.querySelector('.commtext') || node.querySelector('.toptext') || node.classList.contains('commtext') || node.classList.contains('toptext'))) { shouldProcess = true; } } }); } }); if (shouldProcess) { setTimeout(processHNContent, 100); } }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); // Also process when page becomes visible (for back/forward navigation) document.addEventListener('visibilitychange', function() { if (!document.hidden) { setTimeout(processHNContent, 100); } }); })();