您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Utility for GdC.
当前为
// ==UserScript== // @name Diamondberry // @namespace https://greasyfork.org/hexa.cat // @version 1.6---1.5.0-2 // @description Utility for GdC. // @author hexa.cat // @match https://chatroom.talkwithstranger.com/* // @grant none // @run-at document-end // @license MPL 2.0 // ==/UserScript== (function () { /***** IMPORT HIGHLIGHT.JS & STYLES *****/ if (!document.querySelector('link[href*="highlight.js"]')) { const hlStyle = document.createElement('link'); hlStyle.rel = 'stylesheet'; hlStyle.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css'; document.head.appendChild(hlStyle); } if (!window.hljs) { const hlScript = document.createElement('script'); hlScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js'; document.head.appendChild(hlScript); } /***** INJECT CSS *****/ const style = document.createElement('style'); style.textContent = ` @import url(https://fonts.bunny.net/css?family=noto-color-emoji:400); /* Emoji */ .emoji { font-family: "Noto Color Emoji", serif !important; font-weight: 400; font-style: normal; } /* Headers: H1–H6 with custom sizes and weights. Inline elements inside headers inherit the header’s weight. */ h1 { font-size: 1.5em; font-weight: 900 !important; margin: 0.3em 0; } h2 { font-size: 1.4em; font-weight: 800 !important; margin: 0.3em 0; } h3 { font-size: 1.3em; font-weight: 700 !important; margin: 0.3em 0; } h4 { font-size: 1.2em; font-weight: 600 !important; margin: 0.3em 0; } h5 { font-size: 1.1em; font-weight: 600 !important; margin: 0.3em 0; } h6 { font-size: 1em; font-weight: 600 !important; margin: 0.3em 0; } h1 *, h2 *, h3 *, h4 *, h5 *, h6 * { font-weight: inherit !important; } /* Dark themed code block container & copy button */ .code-block-container { position: relative; margin: 0.5em 0; } pre { background: #2F3136; color: #DCDDDE; padding: 8px; overflow: auto; border-radius: 4px; margin: 0; } code { background: transparent; color: inherit; } .hljs { background: transparent !important; color: inherit !important; } .code-copy-button { position: absolute; top: 8px; right: 8px; z-index: 10; background: rgba(0, 0, 0, 0.5); border: none; color: white; padding: 2px 6px; font-size: 0.8em; border-radius: 3px; cursor: pointer; } /* Spoiler styling: 1px black dot as cursor */ .spoiler { background-color: black; color: black; padding: 0 2px; border-radius: 2px; cursor: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="black"/></svg>') 0 0, auto; } .spoiler:hover { color: white; } /* Subtext styling */ .subtext { font-size: 0.9em; color: #777; margin: 0.2em 0; } `; document.head.appendChild(style); // Wait for the DOM to be fully loaded. document.addEventListener('DOMContentLoaded', function() { // Check if the send button exists. const sendButton = document.querySelector('.btn-send'); if (sendButton) { sendButton.addEventListener('click', function() { // Get the editor's content. const editor = document.querySelector('.emojionearea-editor'); if (editor) { // Retrieve the text (works if the editor is a contenteditable div or textarea). let text = editor.innerText || editor.value; // Replace every single backslash with two backslashes. text = text.replace(/\\/g, '\\\\'); // Update the editor with the modified text. // (This means that when the system auto–unescapes a backslash, // it will leave one behind—allowing your inline processing to detect it.) if (editor.innerText !== undefined) { editor.innerText = text; } else { editor.value = text; } } }); } }); /***** INLINE PROCESSING *****/ // The helper functions below work as follows: // // protectEscapes() looks for any occurrence of a backslash followed by any character. // Because the system automatically removes a single backslash, if you want to output // a literal markdown control character (such as "*" or "_") you must type TWO backslashes. // For example, to display a literal asterisk, type "\\*" in your input. The raw text then // becomes "\*", which our function will catch and convert into a placeholder. // Later, restoreEscapes() puts the intended literal character back in place. function protectEscapes(text) { // Match a backslash followed by any character. // Note: due to the system’s behavior, a user must type two backslashes to produce // a raw backslash. For example, to get a literal asterisk, type "\\*". return text.replace(/\\(.)/g, function(match, p1) { return "%%LITERAL_" + p1.charCodeAt(0) + "%%"; }); } function restoreEscapes(text) { return text.replace(/%%LITERAL_(\d+)%%/g, function(match, p1) { return String.fromCharCode(p1); }); } function processInline(text) { // First, protect escaped characters. text = protectEscapes(text); // Process combined bold+italic markers (either "*_" or "_*"). text = text.replace(/(?:\*_|_\*)([\s\S]+?)(?:_\*|\*_)/g, '<strong><em>$1</em></strong>'); // Underline combinations (allowing multiline) text = text.replace(/__\*\*\*([\s\S]+?)\*\*\*__/g, '<u><strong><em>$1</em></strong></u>'); text = text.replace(/__\*\*([\s\S]+?)\*\*__/g, '<u><strong>$1</strong></u>'); text = text.replace(/__\*([\s\S]+?)\*__/g, '<u><em>$1</em></u>'); text = text.replace(/__(.+?)__/g, '<u>$1</u>'); // Bold+italic (triple asterisks), bold (double asterisks), and italics (single asterisk, underscore, single quote, or double quote) text = text.replace(/\*\*\*([\s\S]+?)\*\*\*/g, '<strong><em>$1</em></strong>'); text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); text = text.replace(/\*([\s\S]+?)\*/g, '<em>$1</em>'); text = text.replace(/_([\s\S]+?)_/g, '<em>$1</em>'); text = text.replace(/"([\s\S]+?)"/g, '<em>$1</em>'); // Strikethrough text = text.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>'); // Links (masked and unembeddable) text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer">$1</a>'); text = text.replace(/<((?:https?:\/\/)[^>]+)>/g, '<a href="$1" rel="noopener noreferrer">$1</a>'); // Plain links text = text.replace(/((?:https?:\/\/)[^\s<]+)/g, '<a href="$1" rel="noopener noreferrer">$1</a>'); // Spoiler tags text = text.replace(/\|\|([\s\S]+?)\|\|/g, '<span class="spoiler">$1</span>'); // Finally, restore the escaped characters. text = restoreEscapes(text); return text; } /***** BLOCK-LEVEL PARSING *****/ function parseMarkdown(text) { // 1. Protect code blocks with placeholders. let codeBlockPlaceholders = []; text = text.replace(/```(\w+)?\n([\s\S]*?)\n?```/g, function(match, lang, code) { let placeholder = `%%%CODEBLOCK${codeBlockPlaceholders.length}%%%`; codeBlockPlaceholders.push({ placeholder, lang: lang || '', code }); return placeholder; }); // 2. Protect inline code with placeholders. let inlineCodePlaceholders = []; text = text.replace(/`([^`]+?)`/g, function(match, code) { let placeholder = `%%%INLINECODE${inlineCodePlaceholders.length}%%%`; inlineCodePlaceholders.push({ placeholder, code }); return placeholder; }); // 3. Process headers (H1–H6) and subtext. text = text.replace(/^######\s+(.*)$/gm, function(match, p1) { return '<h6>' + processInline(p1.trim()) + '</h6>'; }); text = text.replace(/^#####\s+(.*)$/gm, function(match, p1) { return '<h5>' + processInline(p1.trim()) + '</h5>'; }); text = text.replace(/^####\s+(.*)$/gm, function(match, p1) { return '<h4>' + processInline(p1.trim()) + '</h4>'; }); text = text.replace(/^###\s+(.*)$/gm, function(match, p1) { return '<h3>' + processInline(p1.trim()) + '</h3>'; }); text = text.replace(/^##\s+(.*)$/gm, function(match, p1) { return '<h2>' + processInline(p1.trim()) + '</h2>'; }); text = text.replace(/^#\s+(.*)$/gm, function(match, p1) { return '<h1>' + processInline(p1.trim()) + '</h1>'; }); text = text.replace(/^-#\s+(.*)$/gm, function(match, p1) { return '<div class="subtext">' + processInline(p1.trim()) + '</div>'; }); // 4. Process block quotes. text = text.replace(/^>>>\s+([\s\S]+?)(?=\n\S|$)/gm, function(match, p1) { return '<blockquote>' + processInline(p1.trim()) + '</blockquote>'; }); text = text.replace(/^>\s+(.*)$/gm, function(match, p1) { return '<blockquote>' + processInline(p1.trim()) + '</blockquote>'; }); // 5. Process the remaining text as paragraphs. // Split on blank lines. let paragraphs = text.split(/\n\s*\n/); for (let i = 0; i < paragraphs.length; i++) { let para = paragraphs[i].trim(); // If the paragraph already starts with a block-level tag, leave it. if (/^<(h[1-6]|blockquote|ul|ol|div|pre)/i.test(para)) { paragraphs[i] = para; } else { // Check if the paragraph is a list. let lines = para.split('\n'); let isUnordered = lines.every(line => /^\s*[-*]\s+/.test(line)); let isOrdered = lines.every(line => /^\s*\d+\.\s+/.test(line)); if (isUnordered) { let out = "<ul>\n"; for (let line of lines) { let item = line.replace(/^\s*[-*]\s+/, ''); out += "<li>" + processInline(item.trim()) + "</li>\n"; } out += "</ul>"; paragraphs[i] = out; } else if (isOrdered) { let out = "<ol>\n"; for (let line of lines) { let item = line.replace(/^\s*\d+\.\s+/, ''); out += "<li>" + processInline(item.trim()) + "</li>\n"; } out += "</ol>"; paragraphs[i] = out; } else { // Normal paragraph: process inline on the entire block and convert internal newlines to <br>. paragraphs[i] = processInline(para).replace(/\n/g, '<br>'); } } } text = paragraphs.join("\n"); // 6. Reinstate inline code placeholders. for (let obj of inlineCodePlaceholders) { text = text.replace(obj.placeholder, `<code>${obj.code}</code>`); } // 7. Reinstate code block placeholders with a copy button. for (let obj of codeBlockPlaceholders) { let replacement; if (obj.lang) { replacement = `<div class="code-block-container"> <button class="code-copy-button">Copy</button> <pre><code class="hljs language-${obj.lang}">${obj.code}</code></pre> </div>`; } else { replacement = `<div class="code-block-container"> <button class="code-copy-button">Copy</button> <pre><code class="hljs">${obj.code}</code></pre> </div>`; } text = text.replace(obj.placeholder, replacement); } return text; } /***** EMOJI WRAPPING *****/ const emojiRegex = /([\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FBA0}-\u{1FBAF}\u{1FAD0}-\u{1FADF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}]+)/gu; function wrapEmojisInTextNode(textNode) { const text = textNode.nodeValue; if (!emojiRegex.test(text)) return; emojiRegex.lastIndex = 0; const parts = text.split(emojiRegex); const fragment = document.createDocumentFragment(); let emojiSpan = null; for (const part of parts) { if (emojiRegex.test(part)) { if (!emojiSpan) { emojiSpan = document.createElement('span'); emojiSpan.className = 'emoji'; } emojiSpan.textContent += part; } else { if (emojiSpan) { fragment.appendChild(emojiSpan); emojiSpan = null; } if (part) { fragment.appendChild(document.createTextNode(part)); } } } if (emojiSpan) { fragment.appendChild(emojiSpan); } textNode.parentNode.replaceChild(fragment, textNode); } function wrapEmojis(node) { if (node.nodeType === Node.TEXT_NODE) { wrapEmojisInTextNode(node); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.classList.contains('emoji')) return; Array.from(node.childNodes).forEach(child => wrapEmojis(child)); } } /***** PROCESS CHAT MESSAGES *****/ function formatChatMessage(el) { if (el.dataset.formatted === 'true') return; // Replace <img class="emojione"> with its alt text. el.querySelectorAll('img.emojione').forEach(img => { const alt = img.getAttribute('alt') || ''; const span = document.createElement('span'); span.className = 'emoji'; span.textContent = alt; img.parentNode.replaceChild(span, img); }); const raw = el.innerText; const html = parseMarkdown(raw); el.innerHTML = html; // Apply highlight.js to code blocks if available. el.querySelectorAll('pre code').forEach(block => { if (window.hljs) { hljs.highlightElement(block); } }); wrapEmojis(el); el.dataset.formatted = 'true'; // Attach copy-button functionality. el.querySelectorAll('.code-copy-button').forEach(button => { button.addEventListener('click', function() { const codeElem = button.parentElement.querySelector('pre code'); if (codeElem) { const codeText = codeElem.innerText; navigator.clipboard.writeText(codeText).then(() => { button.innerText = 'Copied!'; setTimeout(() => { button.innerText = 'Copy'; }, 2000); }); } }); }); } /***** INITIALIZATION & OBSERVER *****/ const isMod = document.querySelector('#is_mod').value === "1"; const originalContentMap = new Map(); document.querySelectorAll('.chat-txt, .file-caption').forEach(el => { if (isMod) { console.log(el.innerText); originalContentMap.set(el, el.innerHTML); } formatChatMessage(el); }); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if ((node.matches('.chat-txt') || node.matches('.file-caption')) && !node.dataset.formatted) { if (isMod) { console.log(node.innerText); originalContentMap.set(node, node.innerHTML); } formatChatMessage(node); } else { node.querySelectorAll('.chat-txt, .file-caption').forEach(el => { if (isMod) { console.log(el.innerText); originalContentMap.set(el, el.innerHTML); } formatChatMessage(el); }); } } }); mutation.target.querySelectorAll('.chat-txt.deleted').forEach(deletedNode => { const chtElement = deletedNode.closest('.cht'); if (chtElement) { if (isMod) { const originalContent = originalContentMap.get(deletedNode); if (originalContent) { deletedNode.innerHTML = originalContent; deletedNode.style.color = 'red'; deletedNode.style.fontWeight = 'bold'; deletedNode.classList.remove('deleted'); } } else { chtElement.style.display = 'none'; } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })();