// ==UserScript== // @name Character Prompt Copy // @namespace http://tampermonkey.net/ // @version 4.0 // @description Finds tags via regex to copy content, ignoring outside text. Includes append-to-buffer functionality. // @author Fey // @match *://2ch.hk/wr/* // @match *://2ch.life/wr/* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // --- 1. Define SVG Icons --- const copyIconSVG = ``; const addIconSVG = ``; const checkIconSVG = ``; // --- 2. Define Local Storage Key & Regex --- const STORAGE_KEY = 'tampermonkey_post_compiler_buffer'; // This regex captures ... blocks. // It's non-greedy (s*?) to handle multiple blocks in one post correctly. const CHARACTER_REGEX = /]*>[\s\S]*?<\/character>/g; // --- 3. Style the buttons --- GM_addStyle(` .post-copy-btn { background: none; border: none; padding: 0 0 0 5px; margin: 0; cursor: pointer; color: #d38d8d; display: inline-flex; align-items: center; vertical-align: middle; transition: all 0.2s ease; } .post-copy-btn:hover { color: #f00; transform: scale(1.1); } .post-copy-btn.copied { color: #78b346; cursor: default; } .post-copy-btn.copied:hover { transform: none; } `); // --- 4. Core Logic --- const addCopyButtons = (postNode) => { if (!postNode || typeof postNode.querySelector !== 'function') return; const targetContainer = postNode.querySelector('.post__details'); if (!targetContainer || targetContainer.querySelector('.post-copy-btn')) return; // A helper function to extract text using the new Regex method. const getTextToCopy = (messageElement) => { if (!messageElement) return ''; // Get the raw HTML content of the post as a string. let postHtml = messageElement.innerText; // Use the regex to find all blocks. const matches = postHtml.match(CHARACTER_REGEX); if (matches && matches.length > 0) { // If found, join the matched blocks. This implicitly ignores all other text. // Replace
tags with newlines for proper formatting in clipboard. return matches.join('\n\n').replace(//gi, '\n'); } else { // If no character tags are found, fall back to copying the visible text. return messageElement.innerText.trim(); } }; // A helper for showing feedback on a button const showFeedback = (button, originalIcon, originalTitle) => { button.innerHTML = checkIconSVG; button.classList.add('copied'); setTimeout(() => { button.innerHTML = originalIcon; button.classList.remove('copied'); button.title = originalTitle; }, 1500); }; // --- Create Button 1: Copy/Replace --- const copyButton = document.createElement('button'); copyButton.className = 'post-copy-btn copy-replace-btn'; copyButton.innerHTML = copyIconSVG; copyButton.title = 'Copy (Replace)'; copyButton.addEventListener('click', (event) => { event.preventDefault(); const messageElement = postNode.querySelector('.post__message'); const postText = getTextToCopy(messageElement); if (postText) { GM_setClipboard(postText); GM_setValue(STORAGE_KEY, postText); // Also resets the buffer copyButton.title = 'Copied! (Buffer reset)'; showFeedback(copyButton, copyIconSVG, 'Copy (Replace)'); } }); // --- Create Button 2: Add/Append --- const addButton = document.createElement('button'); addButton.className = 'post-copy-btn copy-append-btn'; addButton.innerHTML = addIconSVG; const updateAddButtonTitle = () => { const buffer = GM_getValue(STORAGE_KEY, ''); const count = buffer ? buffer.split('\n\n---\n\n').length : 0; addButton.title = `Append (Buffer: ${count} item${count === 1 ? '' : 's'})`; }; updateAddButtonTitle(); // Set initial title addButton.addEventListener('click', (event) => { event.preventDefault(); const messageElement = postNode.querySelector('.post__message'); const postText = getTextToCopy(messageElement); if (postText) { let currentBuffer = GM_getValue(STORAGE_KEY, ''); let newBuffer = currentBuffer ? (currentBuffer + '\n\n---\n\n' + postText) : postText; GM_setValue(STORAGE_KEY, newBuffer); GM_setClipboard(newBuffer); updateAddButtonTitle(); addButton.title = 'Appended & Copied!'; showFeedback(addButton, addIconSVG, addButton.title); // Pass updated title back } }); targetContainer.appendChild(copyButton); targetContainer.appendChild(addButton); }; // --- 5. Execution Logic --- const processAllPosts = () => document.querySelectorAll('.post.post_type_reply').forEach(addCopyButtons); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.matches('.post.post_type_reply')) { addCopyButtons(node); } else { node.querySelectorAll('.post.post_type_reply').forEach(addCopyButtons); } } }); } } }); window.addEventListener('load', () => { processAllPosts(); observer.observe(document.body, { childList: true, subtree: true }); }); })();