// ==UserScript==
// @name ToMD - Convert Content to Markdown
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Convert web page content to Markdown format and copy to clipboard
// @author Steper Lin
// @match *://*/*
// @grant GM_addStyle
// @grant GM_setClipboard
// @require https://cdnjs.cloudflare.com/ajax/libs/turndown/7.1.1/turndown.min.js
// ==/UserScript==
(function() {
'use strict';
// Add styles for the button and notification
GM_addStyle(`
#tomd-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
font-size: 14px;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: all 0.3s ease;
}
#tomd-button:hover {
background-color: #45a049;
box-shadow: 0 3px 7px rgba(0,0,0,0.4);
}
#tomd-notification {
position: fixed;
bottom: 70px;
right: 20px;
background-color: #333;
color: white;
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#tomd-notification.show {
opacity: 1;
}
`);
// Create the ToMD button
const button = document.createElement('button');
button.id = 'tomd-button';
button.textContent = 'ToMD';
document.body.appendChild(button);
// Create notification element
const notification = document.createElement('div');
notification.id = 'tomd-notification';
notification.textContent = 'Markdown copied to clipboard!';
document.body.appendChild(notification);
// Function to show notification
function showNotification(message = 'Markdown copied to clipboard!', duration = 2000) {
notification.textContent = message;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, duration);
}
// Function to detect the main content of the page
function detectMainContent() {
// Try different strategies to find the main content
// 1. Look for article tag
let content = document.querySelector('article');
if (content && content.textContent.trim().length > 100) {
return content;
}
// 2. Look for common content containers
const contentSelectors = [
'div.post-content',
'div.entry-content',
'div.content',
'div.post',
'div.article',
'div.main-content',
'main',
'#content',
'.content',
'.post-body',
'.post-content',
'.article-content'
];
for (const selector of contentSelectors) {
content = document.querySelector(selector);
if (content && content.textContent.trim().length > 100) {
return content;
}
}
// 3. Heuristic approach: Find the element with the most text content
const paragraphs = document.querySelectorAll('p');
if (paragraphs.length > 0) {
// Find which container has the most paragraphs
const containers = new Map();
paragraphs.forEach(p => {
if (p.textContent.trim().length < 20) return; // Skip very short paragraphs
// Find parent elements up to 4 levels
let parent = p.parentElement;
for (let i = 0; i < 4 && parent; i++) {
containers.set(parent, (containers.get(parent) || 0) + 1);
parent = parent.parentElement;
}
});
// Find the container with the most paragraphs
let maxParagraphs = 0;
let mainContainer = null;
containers.forEach((count, container) => {
if (count > maxParagraphs) {
maxParagraphs = count;
mainContainer = container;
}
});
if (mainContainer && maxParagraphs >= 3) {
return mainContainer;
}
}
// 4. Fallback to body if nothing else works
return document.body;
}
// Function to convert HTML to Markdown
function convertToMarkdown(element) {
// Configure TurndownService for better markdown conversion
const service = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
emDelimiter: '*'
});
// Improve list handling
service.addRule('listItems', {
filter: ['li'],
replacement: function (content, node, options) {
content = content.trim().replace(/^\s+|\s+$/g, '');
const isOrdered = node.parentNode.nodeName.toLowerCase() === 'ol';
const prefix = isOrdered ? '1. ' : '- ';
const indent = node.parentNode.parentNode.nodeName.toLowerCase() === 'li' ? ' ' : '';
return indent + prefix + content;
}
});
// Improve image handling
service.addRule('images', {
filter: 'img',
replacement: function (content, node) {
const alt = node.alt || '';
const src = node.getAttribute('src') || '';
const title = node.title || '';
const titlePart = title ? ' "' + title + '"' : '';
return src ? '' : '';
}
});
// Clone the element to avoid modifying the actual page
const clonedElement = element.cloneNode(true);
// Remove unnecessary elements from clone
const elementsToRemove = [
'script',
'style',
'iframe',
'nav',
'header:not(article header)',
'footer:not(article footer)',
'.sidebar',
'.comments',
'.navigation',
'.ads',
'.share-buttons'
];
elementsToRemove.forEach(selector => {
const elements = clonedElement.querySelectorAll(selector);
elements.forEach(el => el.parentNode?.removeChild(el));
});
// Convert the HTML to markdown
return service.turndown(clonedElement.innerHTML);
}
// Button click handler
button.addEventListener('click', function() {
try {
// Find the main content
const mainContent = detectMainContent();
if (!mainContent) {
showNotification('Could not detect main content on this page.', 3000);
return;
}
// Convert content to Markdown
const markdown = convertToMarkdown(mainContent);
if (!markdown || markdown.trim().length === 0) {
showNotification('No content found to convert.', 3000);
return;
}
// Copy to clipboard
GM_setClipboard(markdown);
// Show success notification
showNotification('Markdown copied to clipboard!');
} catch (error) {
console.error('ToMD Error:', error);
showNotification('Error converting to Markdown. See console for details.', 4000);
}
});
})();