Diamondberry

Utility for GdC.

  1. // ==UserScript==
  2. // @name Diamondberry
  3. // @namespace https://greasyfork.org/hexa.cat
  4. // @version 1.7
  5. // @description Utility for GdC.
  6. // @author hexa.cat
  7. // @match https://chatroom.talkwithstranger.com/*
  8. // @grant none
  9. // @run-at document-end
  10. // @license MPL 2.0
  11. // ==/UserScript==
  12. (function () {
  13. /***** IMPORT HIGHLIGHT.JS & STYLES *****/
  14. if (!document.querySelector('link[href*="highlight.js"]')) {
  15. const hlStyle = document.createElement('link');
  16. hlStyle.rel = 'stylesheet';
  17. hlStyle.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css';
  18. document.head.appendChild(hlStyle);
  19. }
  20. if (!window.hljs) {
  21. const hlScript = document.createElement('script');
  22. hlScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js';
  23. document.head.appendChild(hlScript);
  24. }
  25. /***** INJECT CSS *****/
  26. const style = document.createElement('style');
  27. style.textContent = `
  28. @import url(https://fonts.bunny.net/css?family=noto-color-emoji:400);
  29. /* Emoji */
  30. .emoji {
  31. font-family: "Noto Color Emoji", serif !important;
  32. font-weight: 400;
  33. font-style: normal;
  34. }
  35. /* Headers: H1–H6 with custom sizes and weights.
  36. Inline elements inside headers inherit the header’s weight. */
  37. h1 { font-size: 1.5em; font-weight: 900 !important; margin: 0.3em 0; }
  38. h2 { font-size: 1.4em; font-weight: 800 !important; margin: 0.3em 0; }
  39. h3 { font-size: 1.3em; font-weight: 700 !important; margin: 0.3em 0; }
  40. h4 { font-size: 1.2em; font-weight: 600 !important; margin: 0.3em 0; }
  41. h5 { font-size: 1.1em; font-weight: 600 !important; margin: 0.3em 0; }
  42. h6 { font-size: 1em; font-weight: 600 !important; margin: 0.3em 0; }
  43. h1 *, h2 *, h3 *, h4 *, h5 *, h6 * { font-weight: inherit !important; }
  44. /* Dark themed code block container & copy button */
  45. .code-block-container {
  46. position: relative;
  47. margin: 0.5em 0;
  48. }
  49. pre {
  50. background: #2F3136;
  51. color: #DCDDDE;
  52. padding: 8px;
  53. overflow: auto;
  54. border-radius: 4px;
  55. margin: 0;
  56. }
  57. code {
  58. background: transparent;
  59. color: inherit;
  60. }
  61. .hljs {
  62. background: transparent !important;
  63. color: inherit !important;
  64. }
  65. .code-copy-button {
  66. position: absolute;
  67. top: 8px;
  68. right: 8px;
  69. z-index: 10;
  70. background: rgba(0, 0, 0, 0.5);
  71. border: none;
  72. color: white;
  73. padding: 2px 6px;
  74. font-size: 0.8em;
  75. border-radius: 3px;
  76. cursor: pointer;
  77. }
  78. /* Spoiler styling: 1px black dot as cursor */
  79. .spoiler {
  80. background-color: black;
  81. color: black;
  82. padding: 0 2px;
  83. border-radius: 2px;
  84. 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;
  85. }
  86. .spoiler:hover {
  87. color: white;
  88. }
  89. /* Subtext styling */
  90. .subtext { font-size: 0.9em; color: #777; margin: 0.2em 0; }
  91.  
  92. /* Hide .call-alert and .toast-container */
  93. .call-alert { display: none !important; }
  94. .toast-container { display: none !important; }
  95. `;
  96. document.head.appendChild(style);
  97. // Wait for the DOM to be fully loaded.
  98. document.addEventListener('DOMContentLoaded', function() {
  99. // Check if the send button exists.
  100. const sendButton = document.querySelector('.btn-send');
  101. if (sendButton) {
  102. sendButton.addEventListener('click', function() {
  103. // Get the editor's content.
  104. const editor = document.querySelector('.emojionearea-editor');
  105. if (editor) {
  106. // Retrieve the text (works if the editor is a contenteditable div or textarea).
  107. let text = editor.innerText || editor.value;
  108. // Replace every single backslash with two backslashes.
  109. text = text.replace(/\\/g, '\\\\');
  110. // Update the editor with the modified text.
  111. // (This means that when the system auto–unescapes a backslash,
  112. // it will leave one behind—allowing your inline processing to detect it.)
  113. if (editor.innerText !== undefined) {
  114. editor.innerText = text;
  115. } else {
  116. editor.value = text;
  117. }
  118. }
  119. });
  120. }
  121. });
  122.  
  123. /***** INLINE PROCESSING *****/
  124. // The helper functions below work as follows:
  125. //
  126. // protectEscapes() looks for any occurrence of a backslash followed by any character.
  127. // Because the system automatically removes a single backslash, if you want to output
  128. // a literal markdown control character (such as "*" or "_") you must type TWO backslashes.
  129. // For example, to display a literal asterisk, type "\\*" in your input. The raw text then
  130. // becomes "\*", which our function will catch and convert into a placeholder.
  131. // Later, restoreEscapes() puts the intended literal character back in place.
  132. function protectEscapes(text) {
  133. // Match a backslash followed by any character.
  134. // Note: due to the system’s behavior, a user must type two backslashes to produce
  135. // a raw backslash. For example, to get a literal asterisk, type "\\*".
  136. return text.replace(/\\(.)/g, function(match, p1) {
  137. return "%%LITERAL_" + p1.charCodeAt(0) + "%%";
  138. });
  139. }
  140.  
  141. function restoreEscapes(text) {
  142. return text.replace(/%%LITERAL_(\d+)%%/g, function(match, p1) {
  143. return String.fromCharCode(p1);
  144. });
  145. }
  146.  
  147. function processInline(text) {
  148. // First, protect escaped characters.
  149. text = protectEscapes(text);
  150. // Process combined bold+italic markers (either "*_" or "_*").
  151. text = text.replace(/(?:\*_|_\*)([\s\S]+?)(?:_\*|\*_)/g, '<strong><em>$1</em></strong>');
  152. // Underline combinations (allowing multiline)
  153. text = text.replace(/__\*\*\*([\s\S]+?)\*\*\*__/g, '<u><strong><em>$1</em></strong></u>');
  154. text = text.replace(/__\*\*([\s\S]+?)\*\*__/g, '<u><strong>$1</strong></u>');
  155. text = text.replace(/__\*([\s\S]+?)\*__/g, '<u><em>$1</em></u>');
  156. text = text.replace(/__(.+?)__/g, '<u>$1</u>');
  157. // Bold+italic (triple asterisks), bold (double asterisks), and italics (single asterisk, underscore, single quote, or double quote)
  158. text = text.replace(/\*\*\*([\s\S]+?)\*\*\*/g, '<strong><em>$1</em></strong>');
  159. text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  160. text = text.replace(/\*([\s\S]+?)\*/g, '<em>$1</em>');
  161. text = text.replace(/_([\s\S]+?)_/g, '<em>$1</em>');
  162. text = text.replace(/"([\s\S]+?)"/g, '<em>$1</em>');
  163. // Strikethrough
  164. text = text.replace(/~~([\s\S]+?)~~/g, '<del>$1</del>');
  165. // Links (masked and unembeddable)
  166. text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer">$1</a>');
  167. text = text.replace(/<((?:https?:\/\/)[^>]+)>/g, '<a href="$1" rel="noopener noreferrer">$1</a>');
  168. // Plain links
  169. text = text.replace(/((?:https?:\/\/)[^\s<]+)/g, '<a href="$1" rel="noopener noreferrer">$1</a>');
  170. // Spoiler tags
  171. text = text.replace(/\|\|([\s\S]+?)\|\|/g, '<span class="spoiler">$1</span>');
  172. // Finally, restore the escaped characters.
  173. text = restoreEscapes(text);
  174. return text;
  175. }
  176.  
  177.  
  178. /***** BLOCK-LEVEL PARSING *****/
  179. function parseMarkdown(text) {
  180. // 1. Protect code blocks with placeholders.
  181. let codeBlockPlaceholders = [];
  182. text = text.replace(/```(\w+)?\n([\s\S]*?)\n?```/g, function(match, lang, code) {
  183. let placeholder = `%%%CODEBLOCK${codeBlockPlaceholders.length}%%%`;
  184. codeBlockPlaceholders.push({ placeholder, lang: lang || '', code });
  185. return placeholder;
  186. });
  187. // 2. Protect inline code with placeholders.
  188. let inlineCodePlaceholders = [];
  189. text = text.replace(/`([^`]+?)`/g, function(match, code) {
  190. let placeholder = `%%%INLINECODE${inlineCodePlaceholders.length}%%%`;
  191. inlineCodePlaceholders.push({ placeholder, code });
  192. return placeholder;
  193. });
  194. // 3. Process headers (H1–H6) and subtext.
  195. text = text.replace(/^######\s+(.*)$/gm, function(match, p1) {
  196. return '<h6>' + processInline(p1.trim()) + '</h6>';
  197. });
  198. text = text.replace(/^#####\s+(.*)$/gm, function(match, p1) {
  199. return '<h5>' + processInline(p1.trim()) + '</h5>';
  200. });
  201. text = text.replace(/^####\s+(.*)$/gm, function(match, p1) {
  202. return '<h4>' + processInline(p1.trim()) + '</h4>';
  203. });
  204. text = text.replace(/^###\s+(.*)$/gm, function(match, p1) {
  205. return '<h3>' + processInline(p1.trim()) + '</h3>';
  206. });
  207. text = text.replace(/^##\s+(.*)$/gm, function(match, p1) {
  208. return '<h2>' + processInline(p1.trim()) + '</h2>';
  209. });
  210. text = text.replace(/^#\s+(.*)$/gm, function(match, p1) {
  211. return '<h1>' + processInline(p1.trim()) + '</h1>';
  212. });
  213. text = text.replace(/^-#\s+(.*)$/gm, function(match, p1) {
  214. return '<div class="subtext">' + processInline(p1.trim()) + '</div>';
  215. });
  216. // 4. Process block quotes.
  217. text = text.replace(/^>>>\s+([\s\S]+?)(?=\n\S|$)/gm, function(match, p1) {
  218. return '<blockquote>' + processInline(p1.trim()) + '</blockquote>';
  219. });
  220. text = text.replace(/^>\s+(.*)$/gm, function(match, p1) {
  221. return '<blockquote>' + processInline(p1.trim()) + '</blockquote>';
  222. });
  223. // 5. Process the remaining text as paragraphs.
  224. // Split on blank lines.
  225. let paragraphs = text.split(/\n\s*\n/);
  226. for (let i = 0; i < paragraphs.length; i++) {
  227. let para = paragraphs[i].trim();
  228. // If the paragraph already starts with a block-level tag, leave it.
  229. if (/^<(h[1-6]|blockquote|ul|ol|div|pre)/i.test(para)) {
  230. paragraphs[i] = para;
  231. } else {
  232. // Check if the paragraph is a list.
  233. let lines = para.split('\n');
  234. let isUnordered = lines.every(line => /^\s*[-*]\s+/.test(line));
  235. let isOrdered = lines.every(line => /^\s*\d+\.\s+/.test(line));
  236. if (isUnordered) {
  237. let out = "<ul>\n";
  238. for (let line of lines) {
  239. let item = line.replace(/^\s*[-*]\s+/, '');
  240. out += "<li>" + processInline(item.trim()) + "</li>\n";
  241. }
  242. out += "</ul>";
  243. paragraphs[i] = out;
  244. } else if (isOrdered) {
  245. let out = "<ol>\n";
  246. for (let line of lines) {
  247. let item = line.replace(/^\s*\d+\.\s+/, '');
  248. out += "<li>" + processInline(item.trim()) + "</li>\n";
  249. }
  250. out += "</ol>";
  251. paragraphs[i] = out;
  252. } else {
  253. // Normal paragraph: process inline on the entire block and convert internal newlines to <br>.
  254. paragraphs[i] = processInline(para).replace(/\n/g, '<br>');
  255. }
  256. }
  257. }
  258. text = paragraphs.join("\n");
  259. // 6. Reinstate inline code placeholders.
  260. for (let obj of inlineCodePlaceholders) {
  261. text = text.replace(obj.placeholder, `<code>${obj.code}</code>`);
  262. }
  263. // 7. Reinstate code block placeholders with a copy button.
  264. for (let obj of codeBlockPlaceholders) {
  265. let replacement;
  266. if (obj.lang) {
  267. replacement = `<div class="code-block-container">
  268. <button class="code-copy-button">Copy</button>
  269. <pre><code class="hljs language-${obj.lang}">${obj.code}</code></pre>
  270. </div>`;
  271. } else {
  272. replacement = `<div class="code-block-container">
  273. <button class="code-copy-button">Copy</button>
  274. <pre><code class="hljs">${obj.code}</code></pre>
  275. </div>`;
  276. }
  277. text = text.replace(obj.placeholder, replacement);
  278. }
  279. return text;
  280. }
  281. /***** EMOJI WRAPPING *****/
  282. 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;
  283. function wrapEmojisInTextNode(textNode) {
  284. const text = textNode.nodeValue;
  285. if (!emojiRegex.test(text)) return;
  286. emojiRegex.lastIndex = 0;
  287. const parts = text.split(emojiRegex);
  288. const fragment = document.createDocumentFragment();
  289. let emojiSpan = null;
  290. for (const part of parts) {
  291. if (emojiRegex.test(part)) {
  292. if (!emojiSpan) {
  293. emojiSpan = document.createElement('span');
  294. emojiSpan.className = 'emoji';
  295. }
  296. emojiSpan.textContent += part;
  297. } else {
  298. if (emojiSpan) {
  299. fragment.appendChild(emojiSpan);
  300. emojiSpan = null;
  301. }
  302. if (part) {
  303. fragment.appendChild(document.createTextNode(part));
  304. }
  305. }
  306. }
  307. if (emojiSpan) {
  308. fragment.appendChild(emojiSpan);
  309. }
  310. textNode.parentNode.replaceChild(fragment, textNode);
  311. }
  312. function wrapEmojis(node) {
  313. if (node.nodeType === Node.TEXT_NODE) {
  314. wrapEmojisInTextNode(node);
  315. } else if (node.nodeType === Node.ELEMENT_NODE) {
  316. if (node.classList.contains('emoji')) return;
  317. Array.from(node.childNodes).forEach(child => wrapEmojis(child));
  318. }
  319. }
  320. /***** PROCESS CHAT MESSAGES *****/
  321. function formatChatMessage(el) {
  322. if (el.dataset.formatted === 'true') return;
  323. // Replace <img class="emojione"> with its alt text.
  324. el.querySelectorAll('img.emojione').forEach(img => {
  325. const alt = img.getAttribute('alt') || '';
  326. const span = document.createElement('span');
  327. span.className = 'emoji';
  328. span.textContent = alt;
  329. img.parentNode.replaceChild(span, img);
  330. });
  331. const raw = el.innerText;
  332. const html = parseMarkdown(raw);
  333. el.innerHTML = html;
  334. // Apply highlight.js to code blocks if available.
  335. el.querySelectorAll('pre code').forEach(block => {
  336. if (window.hljs) {
  337. hljs.highlightElement(block);
  338. }
  339. });
  340. wrapEmojis(el);
  341. el.dataset.formatted = 'true';
  342. // Attach copy-button functionality.
  343. el.querySelectorAll('.code-copy-button').forEach(button => {
  344. button.addEventListener('click', function() {
  345. const codeElem = button.parentElement.querySelector('pre code');
  346. if (codeElem) {
  347. const codeText = codeElem.innerText;
  348. navigator.clipboard.writeText(codeText).then(() => {
  349. button.innerText = 'Copied!';
  350. setTimeout(() => { button.innerText = 'Copy'; }, 2000);
  351. });
  352. }
  353. });
  354. });
  355. }
  356. /***** INITIALIZATION & OBSERVER *****/
  357. const isMod = document.querySelector('#is_mod').value === "1";
  358. const originalContentMap = new Map();
  359.  
  360. document.querySelectorAll('.chat-txt, .file-caption').forEach(el => {
  361. if (isMod) {
  362. console.log(el.innerText);
  363. originalContentMap.set(el, el.innerHTML);
  364. }
  365. formatChatMessage(el);
  366. });
  367.  
  368. const observer = new MutationObserver(mutations => {
  369. mutations.forEach(mutation => {
  370. mutation.addedNodes.forEach(node => {
  371. if (node.nodeType === Node.ELEMENT_NODE) {
  372. if ((node.matches('.chat-txt') || node.matches('.file-caption')) && !node.dataset.formatted) {
  373. if (isMod) {
  374. console.log(node.innerText);
  375. originalContentMap.set(node, node.innerHTML);
  376. }
  377. formatChatMessage(node);
  378. } else {
  379. node.querySelectorAll('.chat-txt, .file-caption').forEach(el => {
  380. if (isMod) {
  381. console.log(el.innerText);
  382. originalContentMap.set(el, el.innerHTML);
  383. }
  384. formatChatMessage(el);
  385. });
  386. }
  387. }
  388. });
  389. mutation.target.querySelectorAll('.chat-txt.deleted').forEach(deletedNode => {
  390. const chtElement = deletedNode.closest('.cht');
  391. if (chtElement) {
  392. if (isMod) {
  393. const originalContent = originalContentMap.get(deletedNode);
  394. if (originalContent) {
  395. deletedNode.innerHTML = originalContent;
  396. deletedNode.style.color = 'red';
  397. deletedNode.style.fontWeight = 'bold';
  398. deletedNode.classList.remove('deleted');
  399. }
  400. } else {
  401. chtElement.style.display = 'none';
  402. }
  403. }
  404. });
  405. });
  406. });
  407.  
  408. observer.observe(document.body, { childList: true, subtree: true });
  409. let messageCount = 0;
  410.  
  411. function addMexMessage() {
  412. const _chatBox = document.querySelector('.chat-box');
  413. if (_chatBox) {
  414. const _mexMessage = document.createElement('div');
  415. _mexMessage.className = 'chat-txt mex-message';
  416. _mexMessage.style.display = 'none';
  417. _mexMessage.innerText = `
  418. --------------------
  419. This was sent with Diamondberry
  420. Add it here: https://diamondberry.run
  421. Note that whoever sent this doesn't see this message lol`;
  422. _chatBox.appendChild(_mexMessage);
  423. }
  424. }
  425.  
  426. function _formatChatMessage(el) {
  427. if (el.dataset.formatted === 'true') return;
  428.  
  429. // Replace <img class="emojione"> with its alt text.
  430. el.querySelectorAll('img.emojione').forEach(img => {
  431. const alt = img.getAttribute('alt') || '';
  432. const span = document.createElement('span');
  433. span.className = 'emoji';
  434. span.textContent = alt;
  435. img.parentNode.replaceChild(span, img);
  436. });
  437.  
  438. const raw = el.innerText;
  439. const html = parseMarkdown(raw);
  440. el.innerHTML = html;
  441.  
  442. // Apply highlight.js to code blocks if available.
  443. el.querySelectorAll('pre code').forEach(block => {
  444. if (window.hljs) {
  445. hljs.highlightElement(block);
  446. }
  447. });
  448.  
  449. wrapEmojis(el);
  450. el.dataset.formatted = 'true';
  451.  
  452. // Attach copy-button functionality.
  453. el.querySelectorAll('.code-copy-button').forEach(button => {
  454. button.addEventListener('click', function() {
  455. const codeElem = button.parentElement.querySelector('pre code');
  456. if (codeElem) {
  457. const codeText = codeElem.innerText;
  458. navigator.clipboard.writeText(codeText).then(() => {
  459. button.innerText = 'Copied!';
  460. setTimeout(() => { button.innerText = 'Copy'; }, 2000);
  461. });
  462. }
  463. });
  464. });
  465.  
  466. // Check for mex message and hide it
  467. if (el.innerText.toLowerCase().includes('mex')) {
  468. el.style.display = 'none';
  469. }
  470. }
  471.  
  472. document.addEventListener('DOMContentLoaded', function() {
  473. const sendButton = document.querySelector('.btn-send');
  474. if (sendButton) {
  475. sendButton.addEventListener('click', function() {
  476. messageCount++;
  477. if (messageCount % 2 === 1) {
  478. addMexMessage();
  479. }
  480. });
  481. }
  482. });
  483. })();