Guest User

Untitled

a guest
May 23rd, 2025
55
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Reddit BBCode Converter
  3. // @namespace    http://tampermonkey.net/
  4. // @version      2.7
  5. // @description  Adds a floating "RC" button to convert Reddit content to BBCode, with options for nesting and image handling. Deactivating removes buttons.
  6. // @author       You
  7. // @match        *://old.reddit.com/*
  8. // @match        *://www.reddit.com/*
  9. // @match        *://reddit.com/*
  10. // @grant        GM_setValue
  11. // @grant        GM_getValue
  12. // @grant        GM_addStyle
  13. // @run-at       document-idle
  14. // ==/UserScript==
  15.  
  16. (function() {
  17.     'use strict';
  18.  
  19.     let USE_DATA_URL_FOR_IMAGES = GM_getValue('useDataUrlForImages', false);
  20.     let IS_ACTIVE = GM_getValue('isActive', true);
  21.     let observer;
  22.  
  23.     // Add styles for the button and toast notification
  24.     GM_addStyle(`
  25.         #bbcode-toggle-button {
  26.             position: fixed;
  27.             bottom: 20px;
  28.             left: 20px;
  29.             width: 40px;
  30.             height: 40px;
  31.             background-color: #007bff;
  32.             color: white;
  33.             border: none;
  34.             border-radius: 50%;
  35.             font-size: 16px;
  36.             cursor: pointer;
  37.             z-index: 9999;
  38.             display: flex;
  39.             justify-content: center;
  40.             align-items: center;
  41.             box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  42.             font-family: sans-serif; /* Consistent font */
  43.         }
  44.         #bbcode-toggle-button.inactive {
  45.             background-color: #dc3545; /* Red when inactive */
  46.         }
  47.         .bbcode-toast {
  48.             position: fixed;
  49.             top: 50%;
  50.             left: 50%;
  51.             transform: translate(-50%,-50%);
  52.             padding: 10px;
  53.             border-radius: 5px;
  54.             background: rgba(0,0,0,0.7);
  55.             color: white;
  56.             z-index: 10000;
  57.             box-shadow: 0 2px 10px rgba(0,0,0,0.3);
  58.             font-family: sans-serif;
  59.             font-size: 14px;
  60.             text-align: center; /* Center toast text */
  61.         }
  62.         .depth-dropdown {
  63.             position: absolute;
  64.             margin-left: 5px;
  65.             padding: 2px;
  66.             font-size: 12px;
  67.             z-index: 10001;
  68.             border: 1px solid #ccc;
  69.             border-radius: 3px;
  70.             background-color: white;
  71.             box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  72.             font-family: sans-serif;
  73.         }
  74.         .copy-btn, .nested-btn {
  75.             cursor: pointer;
  76.             background: green;
  77.             color: white;
  78.             margin-left: 5px;
  79.             padding: 2px 6px;
  80.             border: none;
  81.             border-radius: 4px;
  82.             font-size: 12px;
  83.             transition: background-color 0.2s ease;
  84.             font-family: sans-serif;
  85.             display: inline-block; /* Ensure they behave well with text */
  86.         }
  87.         .nested-btn {
  88.             background: blue;
  89.         }
  90.         .copy-btn:hover {
  91.             background-color: #0056b3;
  92.         }
  93.         .nested-btn:hover {
  94.             background-color: #0033a0;
  95.         }
  96.     `);
  97.  
  98.     // Create the floating button
  99.     const toggleButton = document.createElement('button');
  100.     toggleButton.id = 'bbcode-toggle-button';
  101.     toggleButton.textContent = 'RC';
  102.     if (!IS_ACTIVE) {
  103.         toggleButton.classList.add('inactive');
  104.     }
  105.     document.body.appendChild(toggleButton);
  106.  
  107.     toggleButton.addEventListener('click', () => {
  108.         IS_ACTIVE = !IS_ACTIVE;
  109.         GM_setValue('isActive', IS_ACTIVE);
  110.         if (IS_ACTIVE) {
  111.             toggleButton.classList.remove('inactive');
  112.             initializeCopyButtons();
  113.         } else {
  114.             toggleButton.classList.add('inactive');
  115.             removeAllCopyButtons();
  116.             if (observer) {
  117.                 observer.disconnect();
  118.             }
  119.         }
  120.     });
  121.  
  122.     function showToast(message) {
  123.         const toast = document.createElement('div');
  124.         toast.textContent = message;
  125.         toast.className = 'bbcode-toast';
  126.         document.body.appendChild(toast);
  127.         setTimeout(() => toast.remove(), 2000);
  128.     }
  129.  
  130.     function htmlToBBCode(html) {
  131.         return html
  132.             .replace(/<a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]+?)<\/a>/g, (m, url, text) => {
  133.                 const cleanUrl = url.replace(/&amp;/g, '&');
  134.                 const cleanText = text.replace(/<(?!\/?(?:b|i|s|u)\b)[^>]*>/gi, '');
  135.                 return `[u][url='${cleanUrl}']${cleanText}[/url][/u]`;
  136.             })
  137.             .replace(/<img[^>]*src=["']([^"']*)["'][^>]*>/g, '[img]$1[/img]')
  138.             .replace(/<(b|strong)>([^<]+)<\/(b|strong)>/g, '[b]$2[/b]')
  139.             .replace(/<(i|em)>([^<]+)<\/(i|em)>/g, '[i]$2[/i]')
  140.             .replace(/<s>([^<]+)<\/s>/g, '[s]$1[/s]')
  141.             .replace(/<blockquote>([\s\S]+?)<\/blockquote>/g, '[quote]$1[/quote]')
  142.             .replace(/<pre>([\s\S]+?)<\/pre>/g, '[code]$1[/code]')
  143.             .replace(/<code>([^<]+)<\/code>/g, '[icode]$1[/icode]')
  144.             .replace(/<ol>([\s\S]+?)<\/ol>/g, (match, listContent) => '[LIST=1]\n' + listContent.replace(/<li>([\s\S]+?)<\/li>/g, '[*] $1\n') + '[/LIST]')
  145.             .replace(/<ul>([\s\S]+?)<\/ul>/g, (match, listContent) => '[LIST]\n' + listContent.replace(/<li>([\s\S]+?)<\/li>/g, '[*] $1\n') + '[/LIST]')
  146.             .replace(/<\/p>|<\s*br\s*\/?>/gi, '\n')
  147.             .replace(/<\/?[^>]+>/g, '')
  148.             .replace(/&amp;/g, '&')
  149.             .replace(/\n{3,}/g, '\n\n')
  150.             .replace(/^\s+|\s+$/g, '');
  151.     }
  152.  
  153.     function extractTextWithLinks(html) {
  154.         const parser = new DOMParser();
  155.         const doc = parser.parseFromString(html, 'text/html');
  156.         let text = doc.body.innerText.trim();
  157.         const links = Array.from(doc.body.querySelectorAll('a'));
  158.         const images = Array.from(doc.body.querySelectorAll('img'));
  159.  
  160.         let reconstructedText = text;
  161.  
  162.         links.forEach(link => {
  163.             const linkText = link.textContent.trim();
  164.             const linkUrl = link.href;
  165.             if (linkText && reconstructedText.includes(linkText)) {
  166.                 reconstructedText = reconstructedText.replace(linkText, `[u][url]${linkUrl}[/url][/u]`);
  167.             }
  168.         });
  169.  
  170.         images.forEach(image => {
  171.             let src = image.src;
  172.             if (USE_DATA_URL_FOR_IMAGES) {
  173.                 src = image.getAttribute('data-url') || src.replace(/preview\.redd\.it/, 'i.redd.it');
  174.             }
  175.             reconstructedText += `\n[img]${src}[/img]`;
  176.         });
  177.  
  178.         return reconstructedText.trim();
  179.     }
  180.  
  181.     function extractPostUrl(element) {
  182.         let url = element.querySelector('a.bylink[href*="/comments/"]')?.href ||
  183.                   element.querySelector('a.title[href*="/comments/"]')?.href;
  184.  
  185.         if (!url) {
  186.             if (element.hasAttribute('data-permalink')) {
  187.                 url = `https://www.reddit.com${element.getAttribute('data-permalink')}`;
  188.             } else {
  189.                 url = window.location.href;
  190.             }
  191.         }
  192.         return url;
  193.     }
  194.  
  195.     function getParentComments(commentElement, maxDepth = 10) {
  196.         const parents = [];
  197.         const seenIds = new Set();
  198.         let currentComment = commentElement.closest('.thing.comment, shreddit-comment');
  199.  
  200.         while (currentComment && parents.length < maxDepth) {
  201.             let parentId = null;
  202.             let parentCommentElement = null;
  203.  
  204.             parentId = currentComment.getAttribute('parentid');
  205.             if (parentId && parentId.startsWith('t1_')) {
  206.                 parentCommentElement = document.querySelector(`shreddit-comment[thingid="${parentId}"]`);
  207.             }
  208.  
  209.             if (!parentCommentElement) {
  210.                 const parentLink = currentComment.querySelector('a[data-parent-id]');
  211.                 if (parentLink) {
  212.                     parentId = parentLink.getAttribute('data-parent-id');
  213.                     if (parentId.startsWith('t1_')) {
  214.                         parentCommentElement = document.querySelector(`.thing.comment[data-fullname="${parentId}"]`);
  215.                     }
  216.                 } else {
  217.                     const parentThing = currentComment.parentElement.closest('.thing.comment');
  218.                     if (parentThing) {
  219.                         parentId = parentThing.getAttribute('data-fullname') || parentThing.id;
  220.                         parentCommentElement = parentThing;
  221.                     }
  222.                 }
  223.             }
  224.  
  225.             if (!parentCommentElement || !parentId || seenIds.has(parentId)) {
  226.                 break;
  227.             }
  228.             seenIds.add(parentId);
  229.  
  230.             // Updated date extraction for comments
  231.             const datetime = parentCommentElement.querySelector('faceplate-timeago time')?.getAttribute('title') || parentCommentElement.querySelector('faceplate-timeago time')?.getAttribute('datetime') || parentCommentElement.querySelector('time')?.getAttribute('title') || parentCommentElement.querySelector('time')?.getAttribute('datetime') || 'Unknown';
  232.             const contentHtml = parentCommentElement.querySelector('.md, div[slot="comment"]');
  233.             const permalink = parentCommentElement.querySelector('a.bylink')?.href || (parentCommentElement.getAttribute('permalink') ? `https://www.reddit.com${parentCommentElement.getAttribute('permalink')}` : window.location.href);
  234.  
  235.             const contentText = contentHtml ? extractTextWithLinks(contentHtml.innerHTML) : 'No content';
  236.             const contentBBCode = htmlToBBCode(contentText);
  237.  
  238.             parents.unshift({ permalink, spoilerContent: `[spoiler="text"]\nCommented on ${datetime}\n\n${contentBBCode}\n[/spoiler]` });
  239.             currentComment = parentCommentElement;
  240.         }
  241.         return parents;
  242.     }
  243.  
  244.     function createDepthDropdown(maxDepth, callback, button) {
  245.         const existingDropdown = document.querySelector('.depth-dropdown');
  246.         if (existingDropdown) existingDropdown.remove();
  247.  
  248.         const dropdown = document.createElement('select');
  249.         dropdown.className = 'depth-dropdown';
  250.  
  251.         for (let i = 0; i <= maxDepth; i++) {
  252.             const option = document.createElement('option');
  253.             option.value = i;
  254.             option.textContent = i === 0 ? 'Only this' : `${i} parent${i > 1 ? 's' : ''}`;
  255.             dropdown.appendChild(option);
  256.         }
  257.  
  258.         dropdown.addEventListener('change', () => {
  259.             callback(parseInt(dropdown.value));
  260.             dropdown.remove();
  261.         });
  262.         dropdown.addEventListener('blur', () => dropdown.remove());
  263.  
  264.         button.insertAdjacentElement('afterend', dropdown);
  265.  
  266.         requestAnimationFrame(() => {
  267.             dropdown.focus();
  268.         });
  269.     }
  270.  
  271.     function createCopyButtons(element) {
  272.         if (!IS_ACTIVE) return;
  273.  
  274.         const existingButtons = element.querySelectorAll('.copy-btn, .nested-btn');
  275.         if (existingButtons.length > 0) return;
  276.  
  277.         const copyButton = document.createElement('button');
  278.         copyButton.textContent = 'πŸ“‹';
  279.         copyButton.title = 'Copy content to BBCode';
  280.         copyButton.className = 'copy-btn';
  281.  
  282.         const nestedButton = document.createElement('button');
  283.         nestedButton.textContent = 'πŸ“‹';
  284.         nestedButton.title = 'Copy content with nested parents';
  285.         nestedButton.className = 'nested-btn';
  286.  
  287.         if (element.tagName.toLowerCase() === 'shreddit-post' || (element.classList.contains('thing') && element.classList.contains('link'))) {
  288.             const url = extractPostUrl(element);
  289.             let header = '', body = '', images = new Set();
  290.  
  291.             const title = element.querySelector('h1[slot="title"], .title > a')?.textContent.trim() || '';
  292.             const flair = element.querySelector('.linkflairlabel span')?.textContent.trim() || '';
  293.  
  294.             // Updated date extraction for posts
  295.             const datetime = element.querySelector('faceplate-timeago time')?.getAttribute('title') || element.querySelector('faceplate-timeago time')?.getAttribute('datetime') || element.querySelector('time')?.getAttribute('title') || element.querySelector('time')?.getAttribute('datetime') || 'Unknown';
  296.  
  297.             const videoUrl = element.dataset.url || element.getAttribute('data-url');
  298.  
  299.             header = `${flair ? `[i][${flair}][/i] ` : ''}[b]${title}[/b]\n\n${url}\n\n`;
  300.  
  301.             // Enhanced selector for images, including new Reddit's shreddit-media-lightbox-listener
  302.             Array.from(element.querySelectorAll('gallery-carousel li img, .media-preview img, .preview img, shreddit-media-lightbox-listener img.preview-img'))
  303.                 .forEach(img => {
  304.                     let src = img.src;
  305.                     if (USE_DATA_URL_FOR_IMAGES) {
  306.                         src = img.getAttribute('data-url') || src.replace(/preview\.redd\.it/, 'i.redd.it');
  307.                     }
  308.                     // Filter out common Reddit static images, video thumbnails/placeholders, and subreddit icons
  309.                     if (src &&
  310.                         !src.includes('redditstatic.com/video-') &&
  311.                         !src.includes('old.reddit.com/static/checkmark.svg') &&
  312.                         !src.includes('www.redditstatic.com/media/') &&
  313.                         !src.includes('b.thumbs.redditmedia.com/') && // Subreddit icons
  314.                         !src.includes('styles.redditmedia.com/t5_')) // More subreddit icons
  315.                         {
  316.                         images.add(src);
  317.                     }
  318.                 });
  319.  
  320.             const textBody = element.querySelector('div[slot="text-body"], .md');
  321.             if (textBody) {
  322.                 body += htmlToBBCode(extractTextWithLinks(textBody.innerHTML));
  323.             }
  324.  
  325.             const imgSection = Array.from(images).map(u => `[img]${u}[/img]`).join('\n');
  326.             const mediaContent = [`Posted on ${datetime}`, ...(imgSection ? [imgSection] : []), ...(videoUrl ? [videoUrl] : [])];
  327.  
  328.             const fullPayload = `${header}[spoiler="text"]\n${mediaContent.join('\n\n')}\n\n${body.trim()}\n[/spoiler]`;
  329.  
  330.             copyButton.addEventListener('click', (e) => {
  331.                 e.preventDefault();
  332.                 e.stopPropagation();
  333.                 navigator.clipboard.writeText(fullPayload.replace(/\n{3,}/g, '\n\n'))
  334.                     .then(() => showToast('Post copied!'))
  335.                     .catch(err => console.error('[Reddit BBCode Tool] Copy failed:', err));
  336.             });
  337.  
  338.             nestedButton.style.display = 'none';
  339.             const target = element.querySelector('div[slot="credit-bar"], .tagline') || element;
  340.             if (target) {
  341.                 target.appendChild(copyButton);
  342.                 target.appendChild(nestedButton);
  343.             }
  344.         } else if (element.tagName.toLowerCase() === 'shreddit-comment' || element.classList.contains('comment')) {
  345.             const permalink = element.getAttribute('permalink') ? `https://www.reddit.com${element.getAttribute('permalink')}` : element.querySelector('a.bylink')?.href || window.location.href;
  346.             const contentHtml = element.querySelector('div[slot="comment"], .md');
  347.  
  348.             if (!contentHtml) {
  349.                 console.log('[Reddit BBCode Tool] No content found for comment');
  350.                 return;
  351.             }
  352.  
  353.             // Updated date extraction for comments
  354.             const datetime = element.querySelector('faceplate-timeago time')?.getAttribute('title') || element.querySelector('faceplate-timeago time')?.getAttribute('datetime') || element.querySelector('time')?.getAttribute('title') || element.querySelector('time')?.getAttribute('datetime') || 'Unknown';
  355.             const contentText = extractTextWithLinks(contentHtml.innerHTML);
  356.             const contentBBCode = htmlToBBCode(contentText);
  357.             const spoilerContent = `[spoiler="text"]\nCommented on ${datetime}\n\n${contentBBCode}\n[/spoiler]`;
  358.  
  359.             copyButton.addEventListener('click', (e) => {
  360.                 e.preventDefault();
  361.                 e.stopPropagation();
  362.                 navigator.clipboard.writeText(`${permalink}\n${spoilerContent}`.replace(/\n{3,}/g, '\n\n'))
  363.                     .then(() => showToast('Comment copied!'))
  364.                     .catch(err => console.error('[Reddit BBCode Tool] Copy failed:', err));
  365.             });
  366.  
  367.             nestedButton.addEventListener('click', (e) => {
  368.                 e.preventDefault();
  369.                 e.stopPropagation();
  370.                 const parents = getParentComments(element);
  371.                 const maxDepth = parents.length;
  372.                 createDepthDropdown(maxDepth, (depth) => {
  373.                     const items = [...parents.slice(0, depth), { permalink, spoilerContent }];
  374.                     const payload = items
  375.                         .map((item, index) => {
  376.                             const indent = 'β”‚ '.repeat(index);
  377.                             const commentLines = `${item.permalink}\n${item.spoilerContent}`.split('\n');
  378.                             return commentLines.map(line => `${indent}${line}`).join('\n');
  379.                         })
  380.                         .join('\n\n').replace(/\n{3,}/g, '\n\n');
  381.  
  382.                     navigator.clipboard.writeText(payload)
  383.                         .then(() => showToast(`Comment with ${depth} parent${depth === 1 ? '' : 's'} copied!`))
  384.                         .catch(err => console.error('[Reddit BBCode Tool] Nested copy failed:', err));
  385.                 }, nestedButton);
  386.             });
  387.  
  388.             const target = element.querySelector('div[slot="actionRow"], .tagline') || element;
  389.             if (target) {
  390.                 target.appendChild(copyButton);
  391.                 target.appendChild(nestedButton);
  392.             }
  393.         }
  394.     }
  395.  
  396.     function removeAllCopyButtons() {
  397.         document.querySelectorAll('.copy-btn, .nested-btn').forEach(btn => btn.remove());
  398.         const existingDropdown = document.querySelector('.depth-dropdown');
  399.         if (existingDropdown) existingDropdown.remove();
  400.     }
  401.  
  402.     function initializeCopyButtons() {
  403.         if (observer) {
  404.             observer.disconnect();
  405.         }
  406.  
  407.         observer = new MutationObserver(mutations => {
  408.             if (!IS_ACTIVE) return;
  409.             mutations.forEach(mutation => {
  410.                 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  411.                     mutation.addedNodes.forEach(node => {
  412.                         if (node.nodeType === 1) {
  413.                             if (node.matches('shreddit-post, shreddit-comment, .thing.link, .thing.comment')) {
  414.                                 createCopyButtons(node);
  415.                             } else {
  416.                                 node.querySelectorAll('shreddit-post, shreddit-comment, .thing.link, .thing.comment')
  417.                                     .forEach(createCopyButtons);
  418.                             }
  419.                         }
  420.                     });
  421.                 }
  422.             });
  423.         });
  424.  
  425.         observer.observe(document.body, { childList: true, subtree: true });
  426.  
  427.         document.querySelectorAll('shreddit-post, shreddit-comment, .thing.link, .thing.comment').forEach(createCopyButtons);
  428.     }
  429.  
  430.     if (IS_ACTIVE) {
  431.         initializeCopyButtons();
  432.     }
  433. })();
Advertisement
Add Comment
Please, Sign In to add comment