// ==UserScript==
// @name 马同学图解数学(markdown)
// @namespace https://www.djxx.online
// @version 2.4
// @description 将马同学图解数学提取出markdown, 作为本地保存, 请先进行购买
// @author 小小小韩
// @license LGPLv3
// @match https://www.matongxue.com/lessons/*/parts/*
// @grant GM_setClipboard
// @supportURL https://greasyfork.org/zh-CN/scripts/499887
// @icon https://matongxue.oss-cn-hangzhou.aliyuncs.com/static/favicon.ico
// ==/UserScript==
(function () {
'use strict';
function addButton() {
const button = document.createElement('button');
button.textContent = '转换并复制Markdown';
button.style.position = 'fixed';
button.style.top = '53px';
button.style.left = '20px';
button.style.padding = '10px 20px';
button.style.zIndex = '1000';
button.style.cursor = 'pointer';
button.style.backgroundColor = '#4CAF50';
button.style.color = 'white';
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
document.body.appendChild(button);
setupButtonBehavior(button);
}
function setupButtonBehavior(element) {
let pressTimer;
let isDragging = false;
let startX, startY;
element.addEventListener('mousedown', function(event) {
startX = event.clientX;
startY = event.clientY;
pressTimer = setTimeout(function() {
isDragging = true;
element.style.cursor = 'move';
}, 200); // 200ms后判定为长按
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - startX;
const deltaY = event.clientY - startY;
const newLeft = element.offsetLeft + deltaX;
const newTop = element.offsetTop + deltaY;
element.style.left = newLeft + 'px';
element.style.top = newTop + 'px';
startX = event.clientX;
startY = event.clientY;
} else if (Math.abs(event.clientX - startX) > 5 || Math.abs(event.clientY - startY) > 5) {
// 如果移动超过5px,取消短按计时器
clearTimeout(pressTimer);
}
}
function onMouseUp(event) {
clearTimeout(pressTimer);
if (!isDragging) {
processContent(); // 短按执行功能
}
isDragging = false;
element.style.cursor = 'pointer';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
// 防止拖动时选中文本
element.addEventListener('dragstart', function(event) {
event.preventDefault();
});
}
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideInUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.notification {
animation: slideInUp 0.3s ease-out;
transition: all 0.3s ease;
}
.fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
#notification-container {
display: flex;
flex-direction: column-reverse;
align-items: flex-end;
}
`;
document.head.appendChild(style);
let notificationQueue = [];
let isProcessingQueue = false;
// 信息框
function showNotification(message) {
notificationQueue.push(message);
if (!isProcessingQueue) {
processNotificationQueue();
}
}
function processNotificationQueue() {
if (notificationQueue.length === 0) {
isProcessingQueue = false;
return;
}
isProcessingQueue = true;
const message = notificationQueue.shift();
const container = document.querySelector('#notification-container') || (() => {
const newContainer = document.createElement('div');
newContainer.id = 'notification-container';
newContainer.style.position = 'fixed';
newContainer.style.right = '20px';
newContainer.style.bottom = '20px';
newContainer.style.zIndex = '1001';
document.body.appendChild(newContainer);
return newContainer;
})();
const notification = document.createElement('div');
notification.textContent = message;
notification.className = 'notification';
notification.style.backgroundColor = 'rgba(76, 175, 80, 0.6)';
notification.style.color = 'white';
notification.style.padding = '10px 20px';
notification.style.borderRadius = '5px';
notification.style.boxShadow = '0 4px 8px rgba(0,0,0,0.5)';
notification.style.marginTop = '10px';
notification.style.transition = 'all 0.3s ease';
container.appendChild(notification);
// 添加消失动画
setTimeout(() => {
notification.classList.add('fade-out');
notification.addEventListener('animationend', () => {
notification.remove();
});
}, 2000); // 信息框存在2秒
// 0.2秒后处理下一个通知
setTimeout(processNotificationQueue, 200);
}
// 清洗不规则的url
function extractURLFromStyle(styleStr) {
const urlMatch = styleStr.match(/url\(["']?(.+?)["']?\)/);
return urlMatch ? urlMatch[1].split('?')[0] : null;
}
// 提取信息转为markdown的函数
function processContent() {
// iframe视频处理函数
function processIframeVideo(videoUrl) {
//<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="${videoUrl}" frameborder="0" allowfullscreen></iframe>
//<div style="position: relative; width: 100%; padding-bottom: 56.25%;">
//<iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="${videoUrl}" frameborder="0" allowfullscreen></iframe>
//</div>
//<iframe src="${videoUrl}" width="560" height="315" frameborder="0" allowfullscreen></iframe>
return `
<iframe style="width: 100%; aspect-ratio: 16 / 9; border: none; background-color: white;" src="${videoUrl}" allowfullscreen></iframe>
`;
}
// video标签视频处理函数
function processVideoTag(videoUrl) {
return `
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; background-color: white;">
<video style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" controls>
<source src="${videoUrl}" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
`;
}
try {
const tabIndexElements = document.querySelectorAll('[tabindex="0"]');
let markdown = '';
Array.from(tabIndexElements).forEach(el => {
const cardBody = el.querySelector('.ant-card-body');
if (!cardBody) {
return;
}
showNotification("开始处理 .ant-card-body 内容。");
// 二级标题
const titleElement = document.querySelector('.ant-dropdown-trigger');
const titleText = titleElement ? titleElement.textContent.trim() : showNotification("标题未找到");
markdown += `## ${titleText}\n`;
const elements = cardBody.querySelectorAll('video, img:not(.ma-tex-img), section, .xgplayer-poster, p:not(.name), ul.description, ma-quote, blockquote, .ma-card');
Array.from(elements).forEach(element => {
// 视频
if (element.closest('.xgplayer-playbackrate')) return;
if (element.tagName.toLowerCase() === 'video' &&
!element.classList.contains('ma-quote') && element.tagName.toLowerCase() !== 'blockquote' &&
!element.closest('.ma-card')) {
const videoUrl = element.getAttribute('src') || extractURLFromStyle(element.style.backgroundImage);
showNotification('存在视频')
markdown += "\n"+processIframeVideo(videoUrl)+"\n";
} else if (element.classList.contains('xgplayer-poster')) {
const backgroundImage = element.style.backgroundImage;
const videoUrl = extractURLFromStyle(backgroundImage);
if (videoUrl) {
showNotification('存在视频')
markdown += "\n"+processVideoTag(videoUrl)+"\n";
}
}
// 图片
else if ((element.tagName.toLowerCase() === 'img' || element.classList.contains('ma-image-img')) &&
!element.classList.contains('ma-quote') &&
!element.closest('.ma-quote') &&
!element.closest('blockquote') &&
!element.closest('.ma-card')) {
const imageUrl = element.src.split('?')[0];
showNotification('存在图片')
markdown += `\n\n\n`;
}
// 文本
else if (element.tagName.toLowerCase() === 'p' && !element.classList.contains('name') &&
!element.classList.contains('ma-quote') && element.tagName.toLowerCase() !== 'blockquote' &&
!element.closest('.ma-card')&& !(element.tagName.toLowerCase() === 'ul' && element.classList.contains('description'))&&!element.closest('.description')) {
let content = element.innerHTML;
content = processLatexInContent(content);
if (element.classList.contains('caption')) {
markdown += `↑ **${content}**\n\n\n`;
} else {
markdown += `${content}\n\n`;
}
}
// 三级标题
else if (element.tagName.toLowerCase() === 'section') {
const strongElement = element.querySelector('strong');
if (strongElement) {
let titleContent = processLatexInContent(strongElement.outerHTML);
titleContent = titleContent.replace(/<[^>]+>/g, '').trim();
// 将行间公式转换为行内公式
titleContent = titleContent.replace(/\$\$(.*?)\$\$/g, '$$1$');
markdown += `\n### ${titleContent}\n`;
}
}
else if (element.classList.contains('ma-quote') || element.tagName.toLowerCase() === 'blockquote' ||
(element.tagName.toLowerCase() === 'ul' && element.classList.contains('description') && !element.closest('.ma-card')) ||
element.classList.contains('ma-card')) {
let content = [];
// 处理ma-card
if (element.classList.contains('ma-card')) {
showNotification('处理ma-card');
let cardContent = processLatexInContent(element.outerHTML);
cardContent = cardContent.replace(/<b[^>]*>(.*?)<\/b>/g, '**$1** '); // 保留粗体
// 处理卡片内的段落和列表
cardContent = cardContent.replace(/<p class="par">([\s\S]*?)<\/p>/g, (match, p1) => {
return '\n' + p1.trim() + '\n';
});
cardContent = cardContent.replace(/<ul class="description">([\s\S]*?)<\/ul>/g, (match, p1) => {
return '\n' + p1.replace(/<li[^>]*>[\s\S]*?<div>([\s\S]*?)<\/div><\/li>/g, (m, content) => {
return '- ' + content.trim() + '\n';
});
});
// 处理ma-image-img
cardContent = cardContent.replace(/<img[^>]*class="ma-image-img"[^>]*src="([^"]+)"[^>]*>/g, (match, src) => {
return `\n[0]})\n`;
});
// 处理caption
cardContent = cardContent.replace(/<p[^>]*class="caption"[^>]*>([\s\S]*?)<\/p>/g, (match, captionContent) => {
return `\n**${captionContent.trim()}**\n\n\n`;
});
// 处理视频
cardContent = cardContent.replace(/<video[^>]*src="([^"]+)"[^>]*>[\s\S]*?<\/video>/g, (match, src) => {
showNotification('存在引用视频');
return "\n"+processIframeVideo(src);
});
cardContent = cardContent.replace(/<div[^>]*class="xgplayer-poster"[^>]*style="[^"]*background-image:\s*url\(([^)]+)\)[^"]*"[^>]*>[\s\S]*?<\/div>/g, (match, src) => {
const videoUrl = extractURLFromStyle(`url(${src})`);
return "\n"+processVideoTag(videoUrl);
});
//cardContent = cardContent.replace(/<(?!\/?div|\/?iframe|\/?video|\/?source|\/?img)[^>]+>/g, '');
//content = cardContent.split('\n').map(line => line.trim()).filter(Boolean).map(line => '> ' + line);
cardContent = cardContent
// 移除特定的空div结构
.replace(/<\/div><div class="ma-python"><div class="ant-card ma-python-output ant-card-small"[^>]*><div class="ant-card-body"[^>]*><div class="ant-card ma-python-output-content small-card-actions ant-card-small"><div class="ant-card-body"[^>]*><\/div><\/div><\/div><\/div><\/div>/g, '')
// 移除figure相关的空div
.replace(/<div class="figure"><div class="ma-image-row">/g, '')
// 移除所有其他HTML标签,除了iframe、video、source和img
.replace(/<(?!\/?(iframe|video|source|img)(?:\s[^>]*)?>)[^>]+>/g, '');
content = cardContent.split('\n').map(line => line.trim()).filter(Boolean).map(line => '> ' + line);
//cardContent = cardContent.replace(/<(?!\/?video|\/?source|\/?img)[^>]+>/g, '');
//content = cardContent.split('\n').map(line => line.trim()).filter(Boolean).map(line => '> ' + line);
}
// 纯引用或引用+无序列表
else if (element.classList.contains('ma-quote') || element.tagName.toLowerCase() === 'blockquote') {
showNotification('处理引用或卡片信息');
let quoteContent = processLatexInContent(element.outerHTML);
quoteContent = quoteContent.replace(/<b[^>]*>(.*?)<\/b>/g, '**$1** ');
quoteContent = quoteContent.replace(/<\/?(p|div)[^>]*>/g, '\n');
// 处理内部的无序列表
let hasInnerList = /<ul[^>]*>/.test(quoteContent);
quoteContent = quoteContent.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/g, (match, p1) => {
return '\n' + p1.replace(/<li[^>]*>[\s\S]*?<div>([\s\S]*?)<\/div><\/li>/g, (m, content) => {
return '- ' + content.trim() + '\n';
});
});
quoteContent = quoteContent.replace(/<[^>]+>/g, '');
content = quoteContent.split('\n').map(line => line.trim()).filter(Boolean);
if (hasInnerList) {
// 引用+无序列表
showNotification('处理引用+无序列表');
content = content.map(line => {
if (line.startsWith('-')) {
return '> ' + line;
} else {
return '> ' + line + '\n>';
}
});
} else {
// 纯引用
showNotification('处理纯引用');
content = content.map(line => '> ' + line);
}
}
// 纯无序列表
else if (element.tagName.toLowerCase() === 'ul' && element.classList.contains('description') &&
!element.classList.contains('ma-quote') && element.tagName.toLowerCase() !== 'blockquote' &&
!element.closest('.ma-card')) {
showNotification('处理纯无序列表');
let listContent = processLatexInContent(element.outerHTML);
listContent = listContent.replace(/<li[^>]*>[\s\S]*?<div>([\s\S]*?)<\/div><\/li>/g, (m, content) => {
return '- ' + content.trim() + '\n';
});
listContent = listContent.replace(/<[^>]+>/g, ''); // 移除其他所有HTML标签
content = listContent.split('\n').map(line => line.trim()).filter(Boolean);
}
if (content.length > 0) {
markdown += content.join('\n') + '\n\n';
}
}
});
});
// 最后统一替换 & 为 &
markdown = markdown.replace(/&/g, '&');
markdown = markdown.replace(/</g, '<');
markdown = markdown.replace(/>/g, '>');
markdown = markdown.replace(/ /g, '');
GM_setClipboard(markdown);
showNotification('Markdown内容已复制到剪贴板。');
} catch (error) {
console.error('错误:', error);
showNotification('处理内容时出错。请查看控制台了解详情。');
}
}
function processLatexInContent(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
// 处理 img.ma-tex-img(行内公式)
const texImgs = doc.querySelectorAll('img.ma-tex-img');
texImgs.forEach(img => {
let latex = img.getAttribute('alt');
latex = decodeHTMLEntities(latex);
const latexMd = `$${latex}$`;
img.outerHTML = latexMd;
});
// 处理 span.ma-tex-span(行间公式)
const texSpans = doc.querySelectorAll('span.ma-tex-span');
texSpans.forEach(span => {
let latex = span.innerHTML.trim();
// 移除开头的 $ 和结尾的 $
latex = latex.replace(/^\$\s*/, '').replace(/\s*\$$/, '');
// 解码 HTML 实体
latex = decodeHTMLEntities(latex);
// 移除所有换行符和多余的空格
latex = latex.replace(/\s+/g, ' ').trim();
const latexMd = `$$${latex}$$`;
span.outerHTML = latexMd;
});
return doc.body.innerHTML;
}
// 辅助函数:解码 HTML 实体
function decodeHTMLEntities(text) {
const textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
// 加载逻辑
window.addEventListener('load', addButton);
})();