Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Reddit BBCode Converter
- // @namespace http://tampermonkey.net/
- // @version 2.7
- // @description Adds a floating "RC" button to convert Reddit content to BBCode, with options for nesting and image handling. Deactivating removes buttons.
- // @author You
- // @match *://old.reddit.com/*
- // @match *://www.reddit.com/*
- // @match *://reddit.com/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_addStyle
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- 'use strict';
- let USE_DATA_URL_FOR_IMAGES = GM_getValue('useDataUrlForImages', false);
- let IS_ACTIVE = GM_getValue('isActive', true);
- let observer;
- // Add styles for the button and toast notification
- GM_addStyle(`
- #bbcode-toggle-button {
- position: fixed;
- bottom: 20px;
- left: 20px;
- width: 40px;
- height: 40px;
- background-color: #007bff;
- color: white;
- border: none;
- border-radius: 50%;
- font-size: 16px;
- cursor: pointer;
- z-index: 9999;
- display: flex;
- justify-content: center;
- align-items: center;
- box-shadow: 0 2px 5px rgba(0,0,0,0.2);
- font-family: sans-serif; /* Consistent font */
- }
- #bbcode-toggle-button.inactive {
- background-color: #dc3545; /* Red when inactive */
- }
- .bbcode-toast {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%,-50%);
- padding: 10px;
- border-radius: 5px;
- background: rgba(0,0,0,0.7);
- color: white;
- z-index: 10000;
- box-shadow: 0 2px 10px rgba(0,0,0,0.3);
- font-family: sans-serif;
- font-size: 14px;
- text-align: center; /* Center toast text */
- }
- .depth-dropdown {
- position: absolute;
- margin-left: 5px;
- padding: 2px;
- font-size: 12px;
- z-index: 10001;
- border: 1px solid #ccc;
- border-radius: 3px;
- background-color: white;
- box-shadow: 0 2px 5px rgba(0,0,0,0.2);
- font-family: sans-serif;
- }
- .copy-btn, .nested-btn {
- cursor: pointer;
- background: green;
- color: white;
- margin-left: 5px;
- padding: 2px 6px;
- border: none;
- border-radius: 4px;
- font-size: 12px;
- transition: background-color 0.2s ease;
- font-family: sans-serif;
- display: inline-block; /* Ensure they behave well with text */
- }
- .nested-btn {
- background: blue;
- }
- .copy-btn:hover {
- background-color: #0056b3;
- }
- .nested-btn:hover {
- background-color: #0033a0;
- }
- `);
- // Create the floating button
- const toggleButton = document.createElement('button');
- toggleButton.id = 'bbcode-toggle-button';
- toggleButton.textContent = 'RC';
- if (!IS_ACTIVE) {
- toggleButton.classList.add('inactive');
- }
- document.body.appendChild(toggleButton);
- toggleButton.addEventListener('click', () => {
- IS_ACTIVE = !IS_ACTIVE;
- GM_setValue('isActive', IS_ACTIVE);
- if (IS_ACTIVE) {
- toggleButton.classList.remove('inactive');
- initializeCopyButtons();
- } else {
- toggleButton.classList.add('inactive');
- removeAllCopyButtons();
- if (observer) {
- observer.disconnect();
- }
- }
- });
- function showToast(message) {
- const toast = document.createElement('div');
- toast.textContent = message;
- toast.className = 'bbcode-toast';
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 2000);
- }
- function htmlToBBCode(html) {
- return html
- .replace(/<a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]+?)<\/a>/g, (m, url, text) => {
- const cleanUrl = url.replace(/&/g, '&');
- const cleanText = text.replace(/<(?!\/?(?:b|i|s|u)\b)[^>]*>/gi, '');
- return `[u][url='${cleanUrl}']${cleanText}[/url][/u]`;
- })
- .replace(/<img[^>]*src=["']([^"']*)["'][^>]*>/g, '[img]$1[/img]')
- .replace(/<(b|strong)>([^<]+)<\/(b|strong)>/g, '[b]$2[/b]')
- .replace(/<(i|em)>([^<]+)<\/(i|em)>/g, '[i]$2[/i]')
- .replace(/<s>([^<]+)<\/s>/g, '[s]$1[/s]')
- .replace(/<blockquote>([\s\S]+?)<\/blockquote>/g, '[quote]$1[/quote]')
- .replace(/<pre>([\s\S]+?)<\/pre>/g, '[code]$1[/code]')
- .replace(/<code>([^<]+)<\/code>/g, '[icode]$1[/icode]')
- .replace(/<ol>([\s\S]+?)<\/ol>/g, (match, listContent) => '[LIST=1]\n' + listContent.replace(/<li>([\s\S]+?)<\/li>/g, '[*] $1\n') + '[/LIST]')
- .replace(/<ul>([\s\S]+?)<\/ul>/g, (match, listContent) => '[LIST]\n' + listContent.replace(/<li>([\s\S]+?)<\/li>/g, '[*] $1\n') + '[/LIST]')
- .replace(/<\/p>|<\s*br\s*\/?>/gi, '\n')
- .replace(/<\/?[^>]+>/g, '')
- .replace(/&/g, '&')
- .replace(/\n{3,}/g, '\n\n')
- .replace(/^\s+|\s+$/g, '');
- }
- function extractTextWithLinks(html) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- let text = doc.body.innerText.trim();
- const links = Array.from(doc.body.querySelectorAll('a'));
- const images = Array.from(doc.body.querySelectorAll('img'));
- let reconstructedText = text;
- links.forEach(link => {
- const linkText = link.textContent.trim();
- const linkUrl = link.href;
- if (linkText && reconstructedText.includes(linkText)) {
- reconstructedText = reconstructedText.replace(linkText, `[u][url]${linkUrl}[/url][/u]`);
- }
- });
- images.forEach(image => {
- let src = image.src;
- if (USE_DATA_URL_FOR_IMAGES) {
- src = image.getAttribute('data-url') || src.replace(/preview\.redd\.it/, 'i.redd.it');
- }
- reconstructedText += `\n[img]${src}[/img]`;
- });
- return reconstructedText.trim();
- }
- function extractPostUrl(element) {
- let url = element.querySelector('a.bylink[href*="/comments/"]')?.href ||
- element.querySelector('a.title[href*="/comments/"]')?.href;
- if (!url) {
- if (element.hasAttribute('data-permalink')) {
- url = `https://www.reddit.com${element.getAttribute('data-permalink')}`;
- } else {
- url = window.location.href;
- }
- }
- return url;
- }
- function getParentComments(commentElement, maxDepth = 10) {
- const parents = [];
- const seenIds = new Set();
- let currentComment = commentElement.closest('.thing.comment, shreddit-comment');
- while (currentComment && parents.length < maxDepth) {
- let parentId = null;
- let parentCommentElement = null;
- parentId = currentComment.getAttribute('parentid');
- if (parentId && parentId.startsWith('t1_')) {
- parentCommentElement = document.querySelector(`shreddit-comment[thingid="${parentId}"]`);
- }
- if (!parentCommentElement) {
- const parentLink = currentComment.querySelector('a[data-parent-id]');
- if (parentLink) {
- parentId = parentLink.getAttribute('data-parent-id');
- if (parentId.startsWith('t1_')) {
- parentCommentElement = document.querySelector(`.thing.comment[data-fullname="${parentId}"]`);
- }
- } else {
- const parentThing = currentComment.parentElement.closest('.thing.comment');
- if (parentThing) {
- parentId = parentThing.getAttribute('data-fullname') || parentThing.id;
- parentCommentElement = parentThing;
- }
- }
- }
- if (!parentCommentElement || !parentId || seenIds.has(parentId)) {
- break;
- }
- seenIds.add(parentId);
- // Updated date extraction for comments
- 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';
- const contentHtml = parentCommentElement.querySelector('.md, div[slot="comment"]');
- const permalink = parentCommentElement.querySelector('a.bylink')?.href || (parentCommentElement.getAttribute('permalink') ? `https://www.reddit.com${parentCommentElement.getAttribute('permalink')}` : window.location.href);
- const contentText = contentHtml ? extractTextWithLinks(contentHtml.innerHTML) : 'No content';
- const contentBBCode = htmlToBBCode(contentText);
- parents.unshift({ permalink, spoilerContent: `[spoiler="text"]\nCommented on ${datetime}\n\n${contentBBCode}\n[/spoiler]` });
- currentComment = parentCommentElement;
- }
- return parents;
- }
- function createDepthDropdown(maxDepth, callback, button) {
- const existingDropdown = document.querySelector('.depth-dropdown');
- if (existingDropdown) existingDropdown.remove();
- const dropdown = document.createElement('select');
- dropdown.className = 'depth-dropdown';
- for (let i = 0; i <= maxDepth; i++) {
- const option = document.createElement('option');
- option.value = i;
- option.textContent = i === 0 ? 'Only this' : `${i} parent${i > 1 ? 's' : ''}`;
- dropdown.appendChild(option);
- }
- dropdown.addEventListener('change', () => {
- callback(parseInt(dropdown.value));
- dropdown.remove();
- });
- dropdown.addEventListener('blur', () => dropdown.remove());
- button.insertAdjacentElement('afterend', dropdown);
- requestAnimationFrame(() => {
- dropdown.focus();
- });
- }
- function createCopyButtons(element) {
- if (!IS_ACTIVE) return;
- const existingButtons = element.querySelectorAll('.copy-btn, .nested-btn');
- if (existingButtons.length > 0) return;
- const copyButton = document.createElement('button');
- copyButton.textContent = 'π';
- copyButton.title = 'Copy content to BBCode';
- copyButton.className = 'copy-btn';
- const nestedButton = document.createElement('button');
- nestedButton.textContent = 'π';
- nestedButton.title = 'Copy content with nested parents';
- nestedButton.className = 'nested-btn';
- if (element.tagName.toLowerCase() === 'shreddit-post' || (element.classList.contains('thing') && element.classList.contains('link'))) {
- const url = extractPostUrl(element);
- let header = '', body = '', images = new Set();
- const title = element.querySelector('h1[slot="title"], .title > a')?.textContent.trim() || '';
- const flair = element.querySelector('.linkflairlabel span')?.textContent.trim() || '';
- // Updated date extraction for posts
- 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';
- const videoUrl = element.dataset.url || element.getAttribute('data-url');
- header = `${flair ? `[i][${flair}][/i] ` : ''}[b]${title}[/b]\n\n${url}\n\n`;
- // Enhanced selector for images, including new Reddit's shreddit-media-lightbox-listener
- Array.from(element.querySelectorAll('gallery-carousel li img, .media-preview img, .preview img, shreddit-media-lightbox-listener img.preview-img'))
- .forEach(img => {
- let src = img.src;
- if (USE_DATA_URL_FOR_IMAGES) {
- src = img.getAttribute('data-url') || src.replace(/preview\.redd\.it/, 'i.redd.it');
- }
- // Filter out common Reddit static images, video thumbnails/placeholders, and subreddit icons
- if (src &&
- !src.includes('redditstatic.com/video-') &&
- !src.includes('old.reddit.com/static/checkmark.svg') &&
- !src.includes('www.redditstatic.com/media/') &&
- !src.includes('b.thumbs.redditmedia.com/') && // Subreddit icons
- !src.includes('styles.redditmedia.com/t5_')) // More subreddit icons
- {
- images.add(src);
- }
- });
- const textBody = element.querySelector('div[slot="text-body"], .md');
- if (textBody) {
- body += htmlToBBCode(extractTextWithLinks(textBody.innerHTML));
- }
- const imgSection = Array.from(images).map(u => `[img]${u}[/img]`).join('\n');
- const mediaContent = [`Posted on ${datetime}`, ...(imgSection ? [imgSection] : []), ...(videoUrl ? [videoUrl] : [])];
- const fullPayload = `${header}[spoiler="text"]\n${mediaContent.join('\n\n')}\n\n${body.trim()}\n[/spoiler]`;
- copyButton.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- navigator.clipboard.writeText(fullPayload.replace(/\n{3,}/g, '\n\n'))
- .then(() => showToast('Post copied!'))
- .catch(err => console.error('[Reddit BBCode Tool] Copy failed:', err));
- });
- nestedButton.style.display = 'none';
- const target = element.querySelector('div[slot="credit-bar"], .tagline') || element;
- if (target) {
- target.appendChild(copyButton);
- target.appendChild(nestedButton);
- }
- } else if (element.tagName.toLowerCase() === 'shreddit-comment' || element.classList.contains('comment')) {
- const permalink = element.getAttribute('permalink') ? `https://www.reddit.com${element.getAttribute('permalink')}` : element.querySelector('a.bylink')?.href || window.location.href;
- const contentHtml = element.querySelector('div[slot="comment"], .md');
- if (!contentHtml) {
- console.log('[Reddit BBCode Tool] No content found for comment');
- return;
- }
- // Updated date extraction for comments
- 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';
- const contentText = extractTextWithLinks(contentHtml.innerHTML);
- const contentBBCode = htmlToBBCode(contentText);
- const spoilerContent = `[spoiler="text"]\nCommented on ${datetime}\n\n${contentBBCode}\n[/spoiler]`;
- copyButton.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- navigator.clipboard.writeText(`${permalink}\n${spoilerContent}`.replace(/\n{3,}/g, '\n\n'))
- .then(() => showToast('Comment copied!'))
- .catch(err => console.error('[Reddit BBCode Tool] Copy failed:', err));
- });
- nestedButton.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- const parents = getParentComments(element);
- const maxDepth = parents.length;
- createDepthDropdown(maxDepth, (depth) => {
- const items = [...parents.slice(0, depth), { permalink, spoilerContent }];
- const payload = items
- .map((item, index) => {
- const indent = 'β '.repeat(index);
- const commentLines = `${item.permalink}\n${item.spoilerContent}`.split('\n');
- return commentLines.map(line => `${indent}${line}`).join('\n');
- })
- .join('\n\n').replace(/\n{3,}/g, '\n\n');
- navigator.clipboard.writeText(payload)
- .then(() => showToast(`Comment with ${depth} parent${depth === 1 ? '' : 's'} copied!`))
- .catch(err => console.error('[Reddit BBCode Tool] Nested copy failed:', err));
- }, nestedButton);
- });
- const target = element.querySelector('div[slot="actionRow"], .tagline') || element;
- if (target) {
- target.appendChild(copyButton);
- target.appendChild(nestedButton);
- }
- }
- }
- function removeAllCopyButtons() {
- document.querySelectorAll('.copy-btn, .nested-btn').forEach(btn => btn.remove());
- const existingDropdown = document.querySelector('.depth-dropdown');
- if (existingDropdown) existingDropdown.remove();
- }
- function initializeCopyButtons() {
- if (observer) {
- observer.disconnect();
- }
- observer = new MutationObserver(mutations => {
- if (!IS_ACTIVE) return;
- mutations.forEach(mutation => {
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === 1) {
- if (node.matches('shreddit-post, shreddit-comment, .thing.link, .thing.comment')) {
- createCopyButtons(node);
- } else {
- node.querySelectorAll('shreddit-post, shreddit-comment, .thing.link, .thing.comment')
- .forEach(createCopyButtons);
- }
- }
- });
- }
- });
- });
- observer.observe(document.body, { childList: true, subtree: true });
- document.querySelectorAll('shreddit-post, shreddit-comment, .thing.link, .thing.comment').forEach(createCopyButtons);
- }
- if (IS_ACTIVE) {
- initializeCopyButtons();
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment