Diamondberry

Utility for GdC.

目前为 2025-02-08 提交的版本。查看 最新版本

// ==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 });
})();