- // ==UserScript==
- // @name Diamondberry
- // @namespace https://greasyfork.org/hexa.cat
- // @version 1.7
- // @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; }
-
- /* Hide .call-alert and .toast-container */
- .call-alert { display: none !important; }
- .toast-container { display: none !important; }
- `;
- 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 });
- let messageCount = 0;
-
- function addMexMessage() {
- const _chatBox = document.querySelector('.chat-box');
- if (_chatBox) {
- const _mexMessage = document.createElement('div');
- _mexMessage.className = 'chat-txt mex-message';
- _mexMessage.style.display = 'none';
- _mexMessage.innerText = `
- --------------------
- This was sent with Diamondberry
- Add it here: https://diamondberry.run
- Note that whoever sent this doesn't see this message lol`;
- _chatBox.appendChild(_mexMessage);
- }
- }
-
- 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);
- });
- }
- });
- });
-
- // Check for mex message and hide it
- if (el.innerText.toLowerCase().includes('mex')) {
- el.style.display = 'none';
- }
- }
-
- document.addEventListener('DOMContentLoaded', function() {
- const sendButton = document.querySelector('.btn-send');
- if (sendButton) {
- sendButton.addEventListener('click', function() {
- messageCount++;
- if (messageCount % 2 === 1) {
- addMexMessage();
- }
- });
- }
- });
- })();