您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
洛谷Markdown复制工具,Ctrl+Alt+C触发
// ==UserScript== // @name Luogu-MarkdownCopy // @namespace https://www.luogu.com.cn/ // @version 1.0 // @description 洛谷Markdown复制工具,Ctrl+Alt+C触发 // @author Doubao AI // @match https://*.luogu.com.cn/* // @match https://*.luogu.org/* // @icon https://www.luogu.com.cn/favicon.ico // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // 存储调试信息 const debugInfo = { lastUrl: '', lastType: '', lastContentLength: 0, lastError: null, extractionSteps: [], // 记录提取步骤 apiResponse: null, // 记录API响应 elementSelectors: [] // 记录使用的选择器 }; // 页面加载完成后初始化 window.addEventListener('load', () => { init(); setupAdvancedObserver(); console.log('洛谷Markdown复制工具已加载 (v4.2)'); console.log('按Ctrl+Alt+C复制内容,输入__luoguCopyDebug()查看调试信息'); }); // 暴露调试函数 window.__luoguCopyDebug = () => debugInfo; function init() { // 确保只绑定一次事件 document.removeEventListener('keydown', handleKeyCombo); document.addEventListener('keydown', handleKeyCombo); } /** * 高级观察者,针对不同页面类型采用不同观察策略 */ function setupAdvancedObserver() { const isProblemPage = /problem|p\/\d+/.test(window.location.href); const isTeamPage = /team/.test(window.location.href); const isDiscussPage = /discuss/.test(window.location.href); const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-md', 'data-markdown'] }; // 观察频率控制 let lastUpdateTime = 0; const observer = new MutationObserver((mutations) => { const now = Date.now(); if (now - lastUpdateTime > 500) { lastUpdateTime = now; if (mutations.some(m => m.addedNodes.length > 0 || m.target.hasAttribute('data-md') || m.target.hasAttribute('data-markdown') )) { init(); } } }); // 针对题目页面加强观察 if (isProblemPage) { const containers = [ document.querySelector('.problem-container'), document.querySelector('.lg-container'), document.querySelector('.main-container'), document.body ].filter(Boolean); containers.forEach(container => observer.observe(container, config)); } else if (isTeamPage) { const teamContainer = document.querySelector('.team-container') || document.body; observer.observe(teamContainer, config); } else if (isDiscussPage) { const discussContainer = document.querySelector('.discuss-container') || document.body; observer.observe(discussContainer, config); } else { observer.observe(document.body, config); } } /** * 处理快捷键 */ async function handleKeyCombo(e) { if (e.ctrlKey && e.altKey && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); e.stopPropagation(); // 重置调试信息 debugInfo.lastUrl = window.location.href; debugInfo.lastError = null; debugInfo.extractionSteps = []; debugInfo.apiResponse = null; debugInfo.elementSelectors = []; try { const loadingAlert = showTemporaryAlert('正在提取内容...', 5000); loadingAlert.element.textContent = '正在提取内容(1/5尝试)'; // 多次尝试提取,增加成功率 let result = null; for (let i = 0; i < 5; i++) { if (i > 0) { loadingAlert.element.textContent = `正在提取内容(${i+1}/5尝试)`; } result = await getMarkdownByPageType(); if (result?.content) break; await new Promise(resolve => setTimeout(resolve, 500 + i * 300)); } clearTimeout(loadingAlert.timeoutId); loadingAlert.element.remove(); if (result?.content) { debugInfo.lastType = result.type; debugInfo.lastContentLength = result.content.length; await copyToClipboard(result.content); showTemporaryAlert(`✅ 已复制${result.type}(${result.content.length}字符)`, 2000); } else { debugInfo.lastError = '未找到可复制的内容'; alert('❌ 未找到可复制的Markdown内容\n请尝试:\n1. 刷新页面\n2. 确保内容已加载完成\n3. 检查控制台获取更多信息'); } } catch (err) { debugInfo.lastError = err.message; alert(`❌ 操作失败: ${err.message}\n请查看控制台获取详细信息`); console.error('复制失败:', err); } } } /** * 显示临时提示 */ function showTemporaryAlert(message, duration) { const alertEl = document.createElement('div'); alertEl.style.position = 'fixed'; alertEl.style.bottom = '20px'; alertEl.style.right = '20px'; alertEl.style.padding = '10px 20px'; alertEl.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; alertEl.style.color = 'white'; alertEl.style.borderRadius = '4px'; alertEl.style.zIndex = '999999'; alertEl.textContent = message; document.body.appendChild(alertEl); const timeoutId = setTimeout(() => { alertEl.remove(); }, duration); return { element: alertEl, timeoutId }; } /** * 复制到剪贴板 */ async function copyToClipboard(text) { if (!text) throw new Error('无内容可复制'); if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.warn('剪贴板API失败,尝试备用方法:', err); } } const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, text.length); try { const success = document.execCommand('copy'); if (!success) throw new Error('无法执行复制命令'); return true; } finally { document.body.removeChild(textarea); } } /** * 根据页面类型获取内容 */ async function getMarkdownByPageType() { const url = window.location.href; // 优先处理题目页面 if (url.includes('problem') || url.match(/p\/\d+/)) { const content = await getProblemContent(); if (content) return { content, type: '题目内容' }; } if (url.includes('team')) { const content = await getTeamContent(); if (content) return { content, type: '团队内容' }; } if (url.includes('discuss')) { const content = await getDiscussionContent(); if (content) return { content, type: '讨论帖' }; } const strategies = [ { test: /article/, handler: getArticleContent, type: '专栏文章' }, { test: /blog(?!.*admin)/i, handler: getBlogContent, type: '博客' }, { test: /user(?!.*notification)/, handler: getUserIntroduction, type: '用户简介' }, { test: /contest(?!.*list|.*edit)/, handler: getContestDescription, type: '比赛描述' }, { test: /training(?!.*edit|.*list)/, handler: getTrainingDescription, type: '训练内容' }, { test: /solution/, handler: getSolutionContent, type: '题解' }, { test: /forum/, handler: getForumContent, type: '论坛内容' }, { test: /.*/, handler: getGeneralMarkdownContent, type: '页面内容' } ]; for (const { test, handler, type } of strategies) { if (test.test(url)) { try { const content = await handler(); if (content) return { content, type }; } catch (err) { console.log(`[${type}]提取失败:`, err.message); } } } return { content: null, type: null }; } /** * 团队页面内容提取 */ async function getTeamContent() { const match = window.location.href.match(/team\/(\d+)/); if (match?.[1]) { try { const res = await fetch(`/api/team/get?id=${match[1]}`); if (res.ok) { const data = await res.json(); const parts = []; if (data.data?.name) parts.push(`# ${data.data.name}`); if (data.data?.description) parts.push('## 团队描述', data.data.description); if (data.data?.announcement) parts.push('## 团队公告', data.data.announcement); if (parts.length > 0) return parts.join('\n\n'); } } catch (err) { console.log('团队API获取失败:', err); } } const teamSelectors = [ { title: '团队描述', selector: '.team-description, .team-info-description' }, { title: '团队公告', selector: '.team-announcement, .team-notice' }, { title: '团队介绍', selector: '.team-intro, .team-profile' } ]; const parts = []; const teamName = document.querySelector('.team-name, .team-title')?.textContent?.trim(); if (teamName) parts.push(`# ${teamName}`); for (const { title, selector } of teamSelectors) { const el = document.querySelector(selector); if (el?.textContent?.trim()) { parts.push(`## ${title}`); const mdContent = el.getAttribute('data-md') || el.getAttribute('data-markdown') || el.getAttribute('data-original'); if (mdContent) { parts.push(mdContent); } else { if (window._feInjection?.markdown?.toMarkdown) { parts.push(window._feInjection.markdown.toMarkdown(el.innerHTML)); } else { parts.push(el.textContent.trim()); } } } } if (parts.length > (teamName ? 1 : 0)) { return parts.join('\n\n'); } const teamPost = document.querySelector('.team-post-content, .team-topic-content'); if (teamPost) { const content = teamPost.getAttribute('data-md') || (window._feInjection?.markdown?.toMarkdown?.(teamPost.innerHTML) || teamPost.textContent.trim()); if (content) return content; } return null; } /** * 讨论帖内容提取 */ async function getDiscussionContent() { const activePost = document.querySelector('.discuss-post.active, .post-item.highlight'); if (activePost) { const content = await extractContentFromElement(activePost); if (content) return content; } const match = window.location.href.match(/discuss\/(\d+)/); if (match?.[1]) { try { const endpoints = [ `/api/discuss/show/${match[1]}`, `/api/discuss/${match[1]}`, `/discuss/${match[1]}/api` ]; for (const endpoint of endpoints) { const res = await fetch(endpoint, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' }); if (res.ok) { const data = await res.json(); if (data.content) return data.content; if (data.data?.content) return data.data.content; if (data.post?.content) return data.post.content; } } } catch (err) { console.log('讨论帖API获取失败:', err); } } const contentSelectors = [ '.discuss-content', '.post-content', '.lg-article', '.markdown-body', '.post-detail-content', '[class*="discuss-post-content"]' ]; for (const selector of contentSelectors) { const el = document.querySelector(selector); if (el) { const content = await extractContentFromElement(el); if (content) return content; } } const scriptTags = document.querySelectorAll('script[type="application/json"], script:not([type])'); for (const script of scriptTags) { try { const jsonData = JSON.parse(script.textContent); const content = findContentInJson(jsonData); if (content) return content; } catch (err) { // 忽略解析错误 } } return null; } /** * 从元素中提取内容的通用方法 */ function extractContentFromElement(el) { const mdAttrs = ['data-md', 'data-markdown', 'data-original', 'data-content']; for (const attr of mdAttrs) { const mdContent = el.getAttribute(attr); if (mdContent && mdContent.trim().length > 0) { debugInfo.extractionSteps.push(`从属性 ${attr} 提取内容`); return mdContent.trim(); } } const childWithMd = el.querySelector('[data-md], [data-markdown], [data-original]'); if (childWithMd) { const content = childWithMd.getAttribute('data-md') || childWithMd.getAttribute('data-markdown') || childWithMd.getAttribute('data-original'); if (content) { debugInfo.extractionSteps.push(`从子元素属性提取内容`); return content.trim(); } } if (window._feInjection?.markdown?.toMarkdown) { try { const mdContent = window._feInjection.markdown.toMarkdown(el.innerHTML); if (mdContent && mdContent.trim().length > 0) { debugInfo.extractionSteps.push(`使用内置工具转换HTML为Markdown`); return mdContent.trim(); } } catch (err) { console.log('转换HTML为Markdown失败:', err); } } const textContent = el.textContent.trim(); if (textContent.length > 0) { debugInfo.extractionSteps.push(`提取纯文本内容`); return textContent; } return null; } /** * 在JSON数据中查找可能的内容 */ function findContentInJson(obj, depth = 0) { if (depth > 5) return null; if (typeof obj === 'object' && obj !== null) { const contentFields = ['content', 'markdown', 'text', 'description', 'postContent']; for (const field of contentFields) { if (obj[field] && typeof obj[field] === 'string' && obj[field].trim().length > 0) { return obj[field].trim(); } } for (const key in obj) { const result = findContentInJson(obj[key], depth + 1); if (result) return result; } } return null; } /** * 专栏/文章内容提取 */ function getArticleContent() { if (window._feInstance?.currentData?.article?.content) { return window._feInstance.currentData.article.content; } const lentilleContext = document.querySelector('#lentille-context'); if (lentilleContext?.text) { try { const data = JSON.parse(lentilleContext.text); return data.data?.article?.content || null; } catch (err) { /* 忽略错误 */ } } const contentEl = document.querySelector('.article-content, .article-markdown'); return contentEl ? extractContentFromElement(contentEl) : null; } /** * 博客内容提取 */ async function getBlogContent() { if (window.BlogGlobals?.blogID) { try { const res = await fetch(`/api/blog/detail/${window.BlogGlobals.blogID}`); const data = await res.json(); if (data.data?.Content) return data.data.Content; } catch (err) { /* 忽略错误 */ } } if (window._feInstance?.currentData?.blog?.content) { return window._feInstance.currentData.blog.content; } const contentEl = document.querySelector('.blog-content, .blog-markdown'); return contentEl ? extractContentFromElement(contentEl) : null; } /** * 题目内容提取(重点优化) */ async function getProblemContent() { let problemId = null; // 增强题目ID提取:支持多种URL格式 const match1 = window.location.href.match(/problem\/([A-Za-z0-9]+)/); const match2 = window.location.href.match(/p\/(\d+)/); const match3 = window.location.href.match(/problem\/show\?p=(\d+)/); if (match1) problemId = match1[1]; if (match2) problemId = match2[1]; if (match3) problemId = match3[1]; debugInfo.extractionSteps.push(`识别到题目ID: ${problemId || '未识别'}`); // 策略1: 从API获取完整数据(支持多组样例) if (problemId) { try { const apiEndpoints = [ `/api/problem/${problemId}`, `/problem/${problemId}/api`, `/p/${problemId}/api` ]; let apiData = null; for (const endpoint of apiEndpoints) { debugInfo.extractionSteps.push(`尝试API: ${endpoint}`); const res = await fetch(endpoint); if (res.ok) { apiData = await res.json(); debugInfo.apiResponse = apiData; // 保存API响应用于调试 if (apiData.data) { debugInfo.extractionSteps.push(`API返回有效数据`); break; } } } if (apiData?.data) { const parts = [`# ${apiData.data.title || '题目'}`]; const sectionStatus = {}; // 添加题目描述 if (apiData.data.description) { parts.push('## 题目描述', apiData.data.description); sectionStatus.description = '已获取'; } else { sectionStatus.description = '缺失'; } // 添加输入格式 if (apiData.data.inputFormat) { parts.push('## 输入格式', apiData.data.inputFormat); sectionStatus.inputFormat = '已获取'; } else { sectionStatus.inputFormat = '缺失'; } // 添加输出格式 if (apiData.data.outputFormat) { parts.push('## 输出格式', apiData.data.outputFormat); sectionStatus.outputFormat = '已获取'; } else { sectionStatus.outputFormat = '缺失'; } // 处理样例(支持多组样例) if (apiData.data.samples && Array.isArray(apiData.data.samples) && apiData.data.samples.length > 0) { apiData.data.samples.forEach((sample, index) => { if (sample.input) { parts.push(`## 样例输入 ${index + 1}`, '```', sample.input, '```'); } if (sample.output) { parts.push(`## 样例输出 ${index + 1}`, '```', sample.output, '```'); } }); sectionStatus.samples = `已获取 ${apiData.data.samples.length} 组`; } // 兼容旧版单一样例格式 else { if (apiData.data.sampleInput) { parts.push('## 样例输入', '```', apiData.data.sampleInput, '```'); sectionStatus.sampleInput = '已获取'; } else { sectionStatus.sampleInput = '缺失'; } if (apiData.data.sampleOutput) { parts.push('## 样例输出', '```', apiData.data.sampleOutput, '```'); sectionStatus.sampleOutput = '已获取'; } else { sectionStatus.sampleOutput = '缺失'; } } // 添加提示 if (apiData.data.hint) { parts.push('## 提示', apiData.data.hint); sectionStatus.hint = '已获取'; } else { sectionStatus.hint = '缺失'; } // 添加数据范围 if (apiData.data.limit) { parts.push('## 数据范围', apiData.data.limit); sectionStatus.limit = '已获取'; } else { sectionStatus.limit = '缺失'; } debugInfo.extractionSteps.push(`API内容提取状态: ${JSON.stringify(sectionStatus)}`); // 检查是否只有描述部分,如果是则尝试页面提取补充 const hasOnlyDescription = Object.keys(sectionStatus).filter(k => k !== 'description').every(k => sectionStatus[k] === '缺失'); if (hasOnlyDescription && sectionStatus.description === '已获取') { debugInfo.extractionSteps.push('API仅返回描述内容,尝试从页面补充其他部分'); const pageParts = await getProblemContentFromPage(); // 合并API描述和页面提取的其他部分 return mergeProblemParts(parts, pageParts); } return parts.filter(Boolean).join('\n\n'); } } catch (err) { debugInfo.extractionSteps.push(`题目API获取失败: ${err.message}`); console.log('题目API获取失败,尝试页面提取:', err); } } // 策略2: 从页面元素提取(增强选择器和处理逻辑) debugInfo.extractionSteps.push('尝试从页面元素提取完整题目内容'); return getProblemContentFromPage(); } /** * 专门从页面元素提取题目内容 */ async function getProblemContentFromPage() { const problemTitle = document.querySelector('.problem-title, .lg-problem-title, .problem-header h1')?.textContent?.trim() || '题目'; const parts = [`# ${problemTitle}`]; // 扩展题目各部分的选择器列表,适配更多页面样式 const sections = { '题目描述': [ '.problem-description', '.lg-problem-description', '.problem-content-description', '[class*="description"]', '#description' ], '输入格式': [ '.problem-input', '.lg-problem-input', '.problem-content-input', '[class*="input-format"]', '#input' ], '输出格式': [ '.problem-output', '.lg-problem-output', '.problem-content-output', '[class*="output-format"]', '#output' ], '样例输入': [ '.problem-sample-input', '.lg-sample-input', '.sample-input', '[class*="sample-input"]', '#sample-input' ], '样例输出': [ '.problem-sample-output', '.lg-sample-output', '.sample-output', '[class*="sample-output"]', '#sample-output' ], '提示': [ '.problem-hint', '.lg-problem-hint', '.problem-content-hint', '[class*="hint"]', '#hint' ], '数据范围': [ '.problem-limit', '.lg-problem-limit', '.problem-content-limit', '[class*="data-range"]', '#limit' ] }; // 提取每个区块内容 for (const [title, selectors] of Object.entries(sections)) { let content = null; // 尝试所有可能的选择器 for (const selector of selectors) { debugInfo.elementSelectors.push(`尝试选择器: ${selector}`); const el = document.querySelector(selector); if (el) { content = extractContentFromElement(el); if (content) { debugInfo.extractionSteps.push(`成功从 ${selector} 提取 ${title}`); break; } } } if (content) { parts.push(`## ${title}`); if (title.includes('样例')) { parts.push('```', content, '```'); } else { parts.push(content); } } else { debugInfo.extractionSteps.push(`未找到 ${title} 内容`); } } // 检查是否有有效内容 if (parts.length > 1) { return parts.join('\n\n'); } // 策略3: 尝试从全局变量提取 if (window._feInstance?.currentData?.problem?.content) { debugInfo.extractionSteps.push('从全局变量获取内容'); return window._feInstance.currentData.problem.content; } // 策略4: 从脚本标签提取 const scriptTags = document.querySelectorAll('script[type="application/json"]'); for (const script of scriptTags) { try { const data = JSON.parse(script.textContent); const content = findContentInJson(data); if (content) { debugInfo.extractionSteps.push('从脚本标签获取内容'); return content; } } catch (err) { /* 忽略错误 */ } } return null; } /** * 合并API和页面提取的题目部分 */ function mergeProblemParts(apiParts, pageParts) { if (!pageParts) return apiParts.join('\n\n'); // 分离标题和内容 const apiTitle = apiParts[0]; const apiContent = apiParts.slice(1).join('\n\n'); // 分离页面提取的各个部分 const pageSections = {}; let currentSection = null; let currentContent = []; for (const part of pageParts.split('\n\n')) { if (part.startsWith('## ')) { if (currentSection) { pageSections[currentSection] = currentContent.join('\n\n'); } currentSection = part.slice(3).trim(); currentContent = []; } else if (currentSection) { currentContent.push(part); } } if (currentSection) { pageSections[currentSection] = currentContent.join('\n\n'); } // 合并结果:保留API的描述,补充页面的其他部分 const mergedParts = [apiTitle]; // 添加API中的描述 mergedParts.push('## 题目描述', apiContent.split('\n\n').find(p => p.startsWith('## 题目描述') ? false : p) || ''); // 添加页面中的其他部分 const sectionOrder = ['输入格式', '输出格式', '样例输入', '样例输出', '提示', '数据范围']; sectionOrder.forEach(section => { if (pageSections[section]) { mergedParts.push(`## ${section}`, pageSections[section]); } }); return mergedParts.filter(Boolean).join('\n\n'); } /** * 其他页面内容提取函数 */ function getSolutionContent() { const match = window.location.href.match(/solution\/(\d+)/); if (match?.[1]) { try { return fetch(`/api/solution/${match[1]}`) .then(res => res.json()) .then(data => data.data?.content || null) .catch(() => null); } catch (err) { /* 忽略错误 */ } } const contentEl = document.querySelector('.solution-content, .solution-article'); return contentEl ? extractContentFromElement(contentEl) : null; } function getForumContent() { const contentEl = document.querySelector('.forum-post-content, .post-body'); return contentEl ? extractContentFromElement(contentEl) : null; } function getUserIntroduction() { if (window._feInstance?.currentData?.user?.introduction) { return window._feInstance.currentData.user.introduction; } const introEl = document.querySelector('.introduction-content, .user-intro'); return introEl ? extractContentFromElement(introEl) : null; } function getContestDescription() { if (window._feInstance?.currentData?.contest?.description) { return window._feInstance.currentData.contest.description; } const descEl = document.querySelector('.contest-description, .contest-info-content'); return descEl ? extractContentFromElement(descEl) : null; } function getTrainingDescription() { if (window._feInstance?.currentData?.training?.description) { return window._feInstance.currentData.training.description; } const descEl = document.querySelector('.training-description, .training-content'); return descEl ? extractContentFromElement(descEl) : null; } function getGeneralMarkdownContent() { const contentSelectors = [ '.lg-content', '.main-content', '.article-content', '.post-content', '.content-wrapper', '[class*="markdown-content"]', '[id*="content"]' ]; for (const selector of contentSelectors) { const contentEl = document.querySelector(selector); if (contentEl) { const content = extractContentFromElement(contentEl); if (content) return content; } } return null; } })();