// ==UserScript== // @name Universal OJ // @namespace http://tampermonkey.net/ // @version 1.0 // @description OJ 代码编辑器 // @author Sunse666 // @match *://www.luogu.com.cn/problem/* // @match *://www.luogu.com.cn/contest/*/problem/* // @match *://codeforces.com/contest/*/problem/* // @match *://codeforces.com/problemset/problem/*/* // @match *://codeforces.com/gym/*/problem/* // @match *://codeforces.com/group/*/contest/*/problem/* // @match *://ac.nowcoder.com/acm/problem/* // @match *://ac.nowcoder.com/acm/contest/* // @match *://*/problem/* // @match *://*/contest/*/problem/* // @grant GM_addStyle // @require https://cdn.bootcdn.net/ajax/libs/highlight.js/11.11.1/highlight.min.js // @require https://cdn.bootcdn.net/ajax/libs/highlight.js/11.11.1/languages/go.min.js // @run-at document-end // ==/UserScript== (function() { 'use strict'; console.log('[Universal OJ] 脚本启动'); //平台配置 const platformConfigs = { luogu: { name: '洛谷', match: /luogu\.com\.cn/, selectors: { title: '.lfe-h1, h1[class*="title"]', content: '.lfe-marked-wrap, .problem-markdown-content', buttonContainer: '.ide-toolbar .actions, .problem .actions', submitBtn: 'a[class*="title"]:has(.fa-paper-plane)', codeEditor: '.cm-editor, .CodeMirror, textarea.lfe-code', problemId: () => { const match = window.location.pathname.match(/\/problem\/(P\d+|[A-Z]+\d+)/); return match ? match[1] : 'unknown'; } }, getProblemInfo: function() { let html = ''; //获取标题 const titleEl = document.querySelector(this.selectors.title); if (titleEl) { html += `

${escapeHtml(titleEl.textContent.trim())}

`; } //获取题目内容 const contentEl = document.querySelector('.problem[data-v-3bac9eed]'); if (contentEl) { const cloned = contentEl.cloneNode(true); cloned.querySelectorAll('button, .lfe-caption, .problem-block-actions, .io-sample').forEach(el => el.remove()); cloned.querySelectorAll('.lfe-h2').forEach(h2 => { h2.style.color = '#cba6f7'; h2.style.fontSize = '14px'; h2.style.marginTop = '15px'; h2.style.marginBottom = '8px'; }); //处理代码块 cloned.querySelectorAll('pre code').forEach(code => { code.style.background = '#11111b'; code.style.padding = '12px'; code.style.borderRadius = '6px'; code.style.color = '#a6e3a1'; code.style.fontSize = '12px'; }); html += `
${cloned.innerHTML}
`; } //处理样例 const samples = document.querySelectorAll('.io-sample'); if (samples.length > 0) { html += `
样例数据
`; samples.forEach((sample, idx) => { const inputEl = sample.querySelector('.io-sample-block:first-child pre'); const outputEl = sample.querySelector('.io-sample-block:last-child pre'); if (inputEl || outputEl) { html += `
`; html += `

样例 ${idx + 1}

`; if (inputEl) { html += `

输入:

`; html += `
${escapeHtml(inputEl.textContent)}
`; } if (outputEl) { html += `

输出:

`; html += `
${escapeHtml(outputEl.textContent)}
`; } html += `
`; } }); } return html || '

无法获取题目信息

'; }, syncCode: function(code) { const cmElement = document.querySelector('.cm-editor'); if (cmElement) { if (cmElement.cmView && cmElement.cmView.view) { const view = cmElement.cmView.view; view.dispatch({ changes: {from: 0, to: view.state.doc.length, insert: code} }); console.log('[Universal OJ] 已通过 CodeMirror 6 API 同步代码'); return true; } const contentEl = cmElement.querySelector('.cm-content[contenteditable="true"]'); if (contentEl) { contentEl.textContent = code; contentEl.dispatchEvent(new Event('input', { bubbles: true })); console.log('[Universal OJ] 已通过 contenteditable 同步代码'); return true; } } const textarea = document.querySelector('textarea[class*="cm-"], textarea.lfe-code'); if (textarea) { textarea.value = code; textarea.dispatchEvent(new Event('input', { bubbles: true })); console.log('[Universal OJ] 已通过 textarea 同步代码'); return true; } return false; } }, nowcoder: { name: '牛客网', match: /nowcoder\.com/, selectors: { title: '.terminal-topic h2.subject-item-title', content: '.terminal-topic', buttonContainer: '.question-operate, .terminal-topic-operation', submitBtn: 'button.runcode-btn, button[type="submit"]', codeEditor: '.CodeMirror, #codeMirror, textarea[name="code"]', problemId: () => { const problemMatch = window.location.pathname.match(/\/acm\/problem\/(\d+)/); if (problemMatch) { return problemMatch[1]; } const contestMatch = window.location.pathname.match(/\/acm\/contest\/(\d+)\/([A-Z])/); if (contestMatch) { return `contest_${contestMatch[1]}_${contestMatch[2]}`; } return 'unknown'; } }, getProblemInfo: function() { let html = ''; function cleanKatexText(element) { const cloned = element.cloneNode(true); cloned.querySelectorAll('.katex-mathml').forEach(el => el.remove()); cloned.querySelectorAll('.katex-html').forEach(el => { const text = el.textContent.trim(); const textNode = document.createTextNode(text); el.parentNode.replaceChild(textNode, el); }); cloned.querySelectorAll('.katex').forEach(el => { const text = el.textContent.trim(); const textNode = document.createTextNode(text); el.parentNode.replaceChild(textNode, el); }); return cloned; } const titleEl = document.querySelector('h2.subject-item-title'); if (titleEl) { const titleText = titleEl.textContent.trim().replace(/只看题目描述.*$/s, '').trim(); html += `

${escapeHtml(titleText)}

`; } const descEl = document.querySelector('.subject-describe .subject-question'); if (descEl) { const cleaned = cleanKatexText(descEl); html += `
`; html += `
题目描述
`; html += `
${cleaned.innerHTML}
`; html += `
`; } const inputDescTitle = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.includes('输入描述')); if (inputDescTitle) { const inputDescContent = inputDescTitle.nextElementSibling; if (inputDescContent && inputDescContent.tagName === 'PRE') { const cleaned = cleanKatexText(inputDescContent); html += `
`; html += `
输入描述
`; html += `
${cleaned.textContent}
`; html += `
`; } } const outputDescTitle = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.includes('输出描述')); if (outputDescTitle) { const outputDescContent = outputDescTitle.nextElementSibling; if (outputDescContent && outputDescContent.tagName === 'PRE') { const cleaned = cleanKatexText(outputDescContent); html += `
`; html += `
输出描述
`; html += `
${cleaned.textContent}
`; html += `
`; } } const exampleSection = document.querySelector('.question-oi'); if (exampleSection) { html += `
示例
`; const inputDiv = exampleSection.querySelector('.question-oi-mod:nth-child(1) .question-oi-cont pre'); const outputDiv = exampleSection.querySelector('.question-oi-mod:nth-child(2) .question-oi-cont pre'); if (inputDiv || outputDiv) { html += `
`; if (inputDiv) { html += `

输入:

`; html += `
${escapeHtml(inputDiv.textContent)}
`; } if (outputDiv) { html += `

输出:

`; html += `
${escapeHtml(outputDiv.textContent)}
`; } html += `
`; } const explanationDiv = exampleSection.querySelector('.question-oi-mod:last-child .question-oi-cont pre'); if (explanationDiv) { const cleaned = cleanKatexText(explanationDiv); html += `

说明:

`; html += `
${cleaned.textContent}
`; } } return html || '

无法获取题目信息

'; }, syncCode: function(code) { const cmElement = document.querySelector('.CodeMirror'); if (cmElement && cmElement.CodeMirror) { cmElement.CodeMirror.setValue(code); console.log('[Universal OJ] 已通过 CodeMirror API 同步代码'); return true; } const cmById = document.getElementById('codeMirror'); if (cmById && cmById.CodeMirror) { cmById.CodeMirror.setValue(code); console.log('[Universal OJ] 已通过 CodeMirror (ID) 同步代码'); return true; } const textarea = document.querySelector('textarea[name="code"]'); if (textarea) { textarea.value = code; textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); console.log('[Universal OJ] 已通过 textarea 同步代码'); return true; } return false; } }, codeforces: { name: 'Codeforces', match: /codeforces\.com/, selectors: { title: '.problem-statement .header .title', content: '.problem-statement', buttonContainer: '.second-level-menu, .sidebox .rtable, .problem-statement .header', submitBtn: 'input[type="submit"]', codeEditor: '.CodeMirror, #sourceCodeTextarea', problemId: () => { const path = window.location.pathname; const m1 = path.match(/\/(contest|gym)\/(\d+)\/problem\/([A-Z]\d?)/); if (m1) return `cf_${m1[2]}${m1[3]}`; const m2 = path.match(/\/problemset\/problem\/(\d+)\/([A-Z]\d?)/); if (m2) return `cf_${m2[1]}${m2[2]}`; return 'cf_unknown'; } }, getProblemInfo: function() { const root = document.querySelector('.problem-statement'); if (!root) return '

无法找到题面

'; const cloned = root.cloneNode(true); const header = cloned.querySelector('.header'); let headerHtml = ''; if (header) { const title = header.querySelector('.title')?.textContent || ''; const time = header.querySelector('.time-limit')?.textContent.replace('time limit per test', '').trim() || ''; const memory = header.querySelector('.memory-limit')?.textContent.replace('memory limit per test', '').trim() || ''; headerHtml = `

${escapeHtml(title)}

⏱️ ${time} 💾 ${memory}
`; header.remove(); } cloned.querySelectorAll('.input-output-copier').forEach(el => el.remove()); cloned.querySelectorAll('.tex-span').forEach(span => { span.style.fontFamily = '"Times New Roman", Times, serif'; span.style.fontSize = '110%'; }); cloned.querySelectorAll('.section-title').forEach(st => { st.style.color = '#cba6f7'; st.style.fontSize = '14px'; st.style.fontWeight = 'bold'; st.style.marginTop = '18px'; st.style.marginBottom = '8px'; st.style.borderLeft = '3px solid #cba6f7'; st.style.paddingLeft = '8px'; }); const sampleTest = cloned.querySelector('.sample-tests'); if (sampleTest) { sampleTest.querySelectorAll('.input, .output').forEach(block => { block.style.marginBottom = '10px'; const title = block.querySelector('.title'); if (title) { title.style.color = '#f9e2af'; title.style.fontSize = '12px'; title.style.marginBottom = '4px'; } const pre = block.querySelector('pre'); if (pre) { pre.style.background = '#11111b'; pre.style.padding = '10px'; pre.style.borderRadius = '6px'; pre.style.color = '#a6e3a1'; pre.style.border = '1px solid #45475a'; pre.style.margin = '0'; } }); } return `
${headerHtml} ${cloned.innerHTML}
`; }, syncCode: function(code) { const cmElement = document.querySelector('.CodeMirror'); if (cmElement && cmElement.CodeMirror) { cmElement.CodeMirror.setValue(code); console.log('[Universal OJ] 已通过 CodeMirror API 同步代码'); return true; } const textarea = document.getElementById('sourceCodeTextarea'); if (textarea) { textarea.value = code; textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); console.log('[Universal OJ] 已通过 textarea 同步代码'); return true; } return false; } } }; function detectPlatform() { for (const [key, config] of Object.entries(platformConfigs)) { if (config.match.test(window.location.href)) { console.log(`[Universal OJ] 检测到平台: ${config.name}`); return { key, ...config }; } } console.log('[Universal OJ] 未检测到支持的平台'); return null; } const currentPlatform = detectPlatform(); if (!currentPlatform) return; GM_addStyle(` .hljs{color:#ffffff;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c} `); GM_addStyle(` .uoj-editor-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 99999; } .uoj-editor-overlay.active { display: flex; } .uoj-editor-container { width: 95%; max-width: 1200px; height: 90vh; background: #1e1e2e; border: 1px solid #45475a; border-radius: 12px; display: flex; flex-direction: column; } .uoj-editor-header { padding: 14px 18px; border-bottom: 1px solid #45475a; display: flex; justify-content: space-between; align-items: center; background: #313244; border-radius: 12px 12px 0 0; } .uoj-editor-header h3 { color: #89b4fa; font-size: 15px; margin: 0; } .uoj-editor-controls { display: flex; gap: 10px; align-items: center; } .uoj-lang-select { padding: 8px 14px; border: 1px solid #45475a; border-radius: 6px; background: #1e1e2e; color: #cdd6f4; font-size: 13px; cursor: pointer; } .uoj-close-btn { width: 32px; height: 32px; border: 1px solid #45475a; border-radius: 6px; background: #313244; color: #a6adc8; font-size: 20px; cursor: pointer; } .uoj-close-btn:hover { border-color: #f38ba8; color: #f38ba8; } .uoj-editor-body { flex: 1; display: flex; overflow: hidden; padding: 15px; gap: 15px; } .uoj-problem-panel { width: 40%; background: #313244; border-radius: 8px; padding: 15px; overflow: auto; color: #cdd6f4; font-size: 13px; line-height: 1.8; } .uoj-problem-panel::-webkit-scrollbar, .uoj-code-editor::-webkit-scrollbar { width: 8px; height: 8px; } .uoj-problem-panel::-webkit-scrollbar-track, .uoj-code-editor::-webkit-scrollbar-track { background: #1e1e2e; border-radius: 4px; } .uoj-problem-panel::-webkit-scrollbar-thumb, .uoj-code-editor::-webkit-scrollbar-thumb { background: #45475a; border-radius: 4px; } .uoj-problem-panel::-webkit-scrollbar-thumb:hover, .uoj-code-editor::-webkit-scrollbar-thumb:hover { background: #585b70; } .uoj-code-panel { flex: 1; display: flex; flex-direction: column; background: #313244; border-radius: 8px; padding: 15px; } .uoj-editor-wrapper { flex: 1; display: flex; overflow: hidden; border: 1px solid #45475a; border-radius: 8px; background: #0d1117; margin-bottom: 12px; position: relative; } .uoj-line-numbers { width: 50px; background: #1e1e2e; border-right: 1px solid #45475a; padding: 12px 8px; text-align: right; color: #6c7086; overflow: hidden; flex-shrink: 0; user-select: none; line-height: 1.5 !important; font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; font-size: 14px !important; box-sizing: border-box; } .uoj-line-numbers div { line-height: 1.5 !important; height: 21px; } .uoj-code-area { flex: 1; position: relative; overflow: hidden; background: #0d1117; } .uoj-code-highlight { position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0; padding: 12px; background: transparent; pointer-events: none; z-index: 1; overflow: hidden; font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; font-size: 14px !important; line-height: 1.5 !important; white-space: pre; word-wrap: normal; box-sizing: border-box; } .uoj-code-highlight code { display: block; margin: 0; padding: 0; background: transparent; font-family: inherit !important; font-size: inherit !important; line-height: inherit !important; white-space: inherit; } .uoj-code-editor { position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0; padding: 12px; background: transparent; color: transparent; caret-color: #ffffff; resize: none; outline: none; z-index: 2; overflow: auto; white-space: pre; word-wrap: normal; font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; font-size: 14px !important; line-height: 1.5 !important; border: none; box-sizing: border-box; letter-spacing: 0; } .uoj-code-editor::selection { background: rgba(137, 180, 250, 0.3); } .uoj-editor-footer { display: flex; justify-content: space-between; align-items: center; } .uoj-editor-info { font-size: 12px; color: #a6adc8; } .uoj-btn { padding: 10px 20px; border-radius: 6px; font-size: 13px; cursor: pointer; border: none; background: #45475a; color: #cdd6f4; } .uoj-btn:hover { background: #89b4fa; color: #1e1e2e; } .uoj-open-btn-luogu { display: inline-flex !important; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid #52c41a; border-radius: 4px; background: transparent; color: #52c41a; font-size: 14px; cursor: pointer; text-decoration: none; transition: all 0.3s; margin-left: 8px; } .uoj-open-btn-luogu:hover { background: #52c41a; color: #fff; } .uoj-open-btn-luogu .icon { display: flex; align-items: center; } .uoj-open-btn-luogu .text { font-weight: 500; } .uoj-problem-panel .tex-span { font-family: 'Times New Roman', serif; font-style: italic; } .uoj-problem-panel .tex-font-style-tt { font-family: 'Courier New', monospace; background: #11111b; padding: 2px 6px; border-radius: 3px; } .cf-content-wrapper p { margin-bottom: 12px; } .cf-content-wrapper ul { padding-left: 20px; margin-bottom: 12px; } .cf-content-wrapper .tex-span { padding: 0 2px; color: #f5e0dc; } .cf-content-wrapper .tex-font-style-it { font-style: italic; } .cf-content-wrapper .tex-font-style-bf { font-weight: bold; } .cf-content-wrapper pre, .cf-content-wrapper code { white-space: pre-wrap; word-break: break-all; } .problem-statement > div:nth-child(2) { font-size: 13px; margin-bottom: 20px; } .hljs-variable-enhanced { color: #79c0ff !important; font-weight: 500 !important; } .hljs-operator-enhanced { color: #ff7b72 !important; font-weight: bold !important; } .hljs-type-enhanced { color: #ffa657 !important; font-weight: 600 !important; } .hljs-variable-enhanced { color: #79c0ff !important; font-weight: 500 !important; } .bracket-level-0 { color: #FFD700; font-weight: bold; } .bracket-level-1 { color: #DA70D6; font-weight: bold; } .bracket-level-2 { color: #87CEEB; font-weight: bold; } .bracket-level-3 { color: #98FB98; font-weight: bold; } .bracket-level-4 { color: #FFA07A; font-weight: bold; } .bracket-level-5 { color: #F0E68C; font-weight: bold; } `); const codeTemplates = { c: ``, cpp: ``, go: ``, java: ``, javascript: ``, python: `` }; const langNames = { c: 'C', cpp: 'C++', go: 'Go', java: 'Java', javascript: 'JavaScript', python: 'Python 3' }; const langClasses = { c: 'language-c', cpp: 'language-cpp', go: 'language-go', java: 'language-java', javascript: 'language-javascript', python: 'language-python' }; let currentLang = 'cpp'; let problemId = null; function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function createLightbox() { if (document.getElementById('uojEditorOverlay')) return; const div = document.createElement('div'); div.className = 'uoj-editor-overlay'; div.id = 'uojEditorOverlay'; div.innerHTML = `

✏️ 代码编辑器 - ${currentPlatform.name}

1
`; document.body.appendChild(div); //事件绑定 div.addEventListener('click', (e) => { if (e.target.dataset.action === 'close' || e.target === div) { closeEditor(); } }); document.getElementById('uojLangSelect').addEventListener('change', changeLanguage); document.getElementById('uojConfirmBtn').addEventListener('click', handleConfirm); const editor = document.getElementById('uojCodeEditor'); editor.addEventListener('input', () => { updateCode(); syncScroll(); }); editor.addEventListener('scroll', syncScroll); editor.addEventListener('keydown', handleKey); editor.addEventListener('click', updateCursorInfo); editor.addEventListener('keyup', updateCursorInfo); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeEditor(); }); if (currentPlatform.key === 'nowcoder') { GM_addStyle(` pre code.hljs { padding: 0 !important; } .uoj-code-highlight { padding: 10px 0 10px 10px !important; margin: 0 !important; } .uoj-code-highlight code { display: block; line-height: 1.5 !important; font-size: 14px !important; font-family: 'Consolas', 'Monaco', monospace !important; } .uoj-code-editor { padding: 12px 12px 12px 12px !important; margin: 0 !important; line-height: 1.5 !important; font-size: 14px !important; font-family: 'Consolas', 'Monaco', monospace !important; text-indent: 0 !important; border: none !important; } .uoj-line-numbers { padding: 12px 8px 12px 8px !important; line-height: 1.5 !important; font-size: 14px !important; } .uoj-line-numbers div { line-height: 1.5 !important; height: 21px !important; margin: 0 !important; padding: 0 !important; } `); } else if (currentPlatform.key === 'codeforces') { GM_addStyle(` .uoj-code-editor, .uoj-code-highlight, .uoj-line-numbers { font-family: 'Courier New', 'Courier', monospace !important; line-height: 1.6 !important; } .uoj-line-numbers div { height: 22.4px; } `); } else if (currentPlatform.key === 'luogu') { GM_addStyle(` .uoj-code-highlight, .uoj-code-highlight *, .uoj-code-highlight code, .uoj-code-highlight code * { color: #ffffff !important; } .uoj-code-highlight .hljs-keyword, .uoj-code-highlight .hljs-type, .uoj-code-highlight .hljs-built_in { color: #ff7b72 !important; } .uoj-code-highlight .hljs-string, .uoj-code-highlight .hljs-regexp { color: #a5d6ff !important; } .uoj-code-highlight .hljs-comment, .uoj-code-highlight .hljs-formula { color: #8b949e !important; } .uoj-code-highlight .hljs-number, .uoj-code-highlight .hljs-literal { color: #79c0ff !important; } .uoj-code-highlight .hljs-title, .uoj-code-highlight .hljs-function { color: #d2a8ff !important; } .uoj-code-highlight .hljs-variable-enhanced { color: #79c0ff !important; font-weight: 500 !important; } .uoj-code-highlight .hljs-operator-enhanced { color: #ff7b72 !important; font-weight: bold !important; } .uoj-code-highlight .hljs-type-enhanced { color: #ffa657 !important; font-weight: 600 !important; } .uoj-code-highlight .bracket-level-0 { color: #FFD700 !important; } .uoj-code-highlight .bracket-level-1 { color: #DA70D6 !important; } .uoj-code-highlight .bracket-level-2 { color: #87CEEB !important; } .uoj-code-highlight .bracket-level-3 { color: #98FB98 !important; } .uoj-code-highlight .bracket-level-4 { color: #FFA07A !important; } .uoj-code-highlight .bracket-level-5 { color: #F0E68C !important; } `); } } function openEditor() { problemId = currentPlatform.selectors.problemId(); console.log('[Universal OJ] 打开编辑器,题目ID:', problemId); createLightbox(); const overlay = document.getElementById('uojEditorOverlay'); overlay.classList.add('active'); document.body.style.overflow = 'hidden'; document.getElementById('uojProblemContent').innerHTML = currentPlatform.getProblemInfo(); const saved = localStorage.getItem(`uoj_code_${problemId}_${currentLang}`); const editor = document.getElementById('uojCodeEditor'); editor.value = saved || codeTemplates[currentLang]; updateLineNumbers(); updateHighlight(); updateCursorInfo(); setTimeout(() => editor.focus(), 100); setTimeout(() => { if (window.MathJax && window.MathJax.typeset) { window.MathJax.typeset([document.getElementById('uojProblemContent')]); } else if (window.MathJax && window.MathJax.Hub && window.MathJax.Hub.Queue) { window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub, "uojProblemContent"]); } }, 500); } function closeEditor() { const overlay = document.getElementById('uojEditorOverlay'); if (overlay) { overlay.classList.remove('active'); document.body.style.overflow = ''; } } function handleConfirm() { const editor = document.getElementById('uojCodeEditor'); const success = currentPlatform.syncCode(editor.value); const btn = document.getElementById('uojConfirmBtn'); const originalText = btn.innerHTML; if (success) { btn.innerHTML = '✅ 已载入'; setTimeout(() => { btn.innerHTML = originalText; closeEditor(); }, 800); } else { btn.innerHTML = '❌ 失败'; setTimeout(() => { btn.innerHTML = originalText; }, 1500); alert('代码同步失败,请手动复制粘贴'); } } function changeLanguage() { const editor = document.getElementById('uojCodeEditor'); const select = document.getElementById('uojLangSelect'); localStorage.setItem(`uoj_code_${problemId}_${currentLang}`, editor.value); currentLang = select.value; const saved = localStorage.getItem(`uoj_code_${problemId}_${currentLang}`); editor.value = saved || codeTemplates[currentLang]; updateLineNumbers(); updateHighlight(); updateCursorInfo(); } function updateCode() { updateLineNumbers(); updateHighlight(); updateCursorInfo(); const editor = document.getElementById('uojCodeEditor'); if (editor) { localStorage.setItem(`uoj_code_${problemId}_${currentLang}`, editor.value); } } function updateHighlight() { const editor = document.getElementById('uojCodeEditor'); const highlightCode = document.getElementById('uojHighlightCode'); if (!editor || !highlightCode) return; let content = editor.value; if (content.endsWith('\n')) { highlightCode.textContent = content + ' '; } else { highlightCode.textContent = content + '\n '; } highlightCode.removeAttribute('data-highlighted'); highlightCode.className = `${langClasses[currentLang]} hljs`; try { hljs.highlightElement(highlightCode); requestAnimationFrame(() => { const code = document.getElementById('uojHighlightCode'); if (code) { applyAllEnhancements(code); } }); } catch (e) { console.error('[Universal OJ] 高亮失败:', e); } syncScroll(); } function applyAllEnhancements(element) { applyBracketColors(element); applyStdTypes(element); applyOperators(element); applyVariableHighlight(element); } function applyBracketColors(element) { const brackets = ['{', '}', '[', ']', '(', ')']; const colors = 6; let stack = []; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; if (!/[(){}\[\]]/.test(text)) return; const fragment = document.createDocumentFragment(); let lastIdx = 0; for (let i = 0; i < text.length; i++) { const char = text[i]; if (brackets.includes(char)) { if (i > lastIdx) { fragment.appendChild(document.createTextNode(text.substring(lastIdx, i))); } const span = document.createElement('span'); let level = 0; if (['{', '[', '('].includes(char)) { level = stack.length % colors; stack.push({ char, level }); } else { const pair = { '}': '{', ']': '[', ')': '(' }; if (stack.length > 0 && stack[stack.length - 1].char === pair[char]) { level = stack.pop().level; } else { level = stack.length % colors; } } span.className = `bracket-level-${level}`; span.textContent = char; fragment.appendChild(span); lastIdx = i + 1; } } if (lastIdx < text.length) { fragment.appendChild(document.createTextNode(text.substring(lastIdx))); } if (lastIdx > 0) { node.parentNode.replaceChild(fragment, node); } } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.className && node.className.includes('bracket-level')) return; const children = Array.from(node.childNodes); children.forEach(processNode); } } processNode(element); } function applyStdTypes(element) { const typeMap = { cpp: ['vector', 'string', 'map', 'set', 'unordered_map', 'unordered_set', 'list', 'deque', 'queue', 'stack', 'priority_queue', 'pair', 'cin', 'cout', 'cerr', 'endl', 'nullptr', 'NULL'], c: ['NULL', 'size_t', 'FILE'], java: ['String', 'Integer', 'ArrayList', 'HashMap', 'Scanner', 'System'], python: ['list', 'dict', 'set', 'tuple', 'str', 'True', 'False', 'None'], go: ['string', 'map', 'slice', 'nil', 'make', 'len', 'append'], javascript: ['console', 'Array', 'Object', 'String', 'Number'] }; const types = typeMap[currentLang] || []; if (types.length === 0) return; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; const matches = []; types.forEach(type => { const regex = new RegExp(`\\b${type}\\b`, 'g'); let match; while ((match = regex.exec(text)) !== null) { matches.push({ start: match.index, end: match.index + type.length, text: type }); } }); if (matches.length === 0) return; matches.sort((a, b) => a.start - b.start); const filtered = []; let lastEnd = -1; matches.forEach(m => { if (m.start >= lastEnd) { filtered.push(m); lastEnd = m.end; } }); if (filtered.length === 0) return; const fragment = document.createDocumentFragment(); let pos = 0; filtered.forEach(m => { if (m.start > pos) { fragment.appendChild(document.createTextNode(text.substring(pos, m.start))); } const span = document.createElement('span'); span.className = 'hljs-type-enhanced'; span.textContent = m.text; fragment.appendChild(span); pos = m.end; }); if (pos < text.length) { fragment.appendChild(document.createTextNode(text.substring(pos))); } node.parentNode.replaceChild(fragment, node); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.className && ( node.className.includes('hljs-string') || node.className.includes('hljs-comment') || node.className.includes('hljs-type-enhanced') || node.className.includes('bracket-level') )) return; Array.from(node.childNodes).forEach(processNode); } } processNode(element); } function applyOperators(element) { const operators = [ '==', '!=', '<=', '>=', '&&', '||', '<<', '>>', '++', '--', '+=', '-=', '*=', '/=', '%=', '->', '::', '=', '+', '-', '*', '/', '%', '<', '>', '!', '&', '|', '^', '~' ]; operators.sort((a, b) => b.length - a.length); function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; if (!/[=!<>&|+\-*\/%~^]/.test(text)) return; const fragment = document.createDocumentFragment(); let pos = 0; while (pos < text.length) { let matched = false; for (const op of operators) { if (text.substr(pos, op.length) === op) { const span = document.createElement('span'); span.className = 'hljs-operator-enhanced'; span.textContent = op; fragment.appendChild(span); pos += op.length; matched = true; break; } } if (!matched) { const nextOpPos = text.slice(pos).search(/[=!<>&|+\-*\/%~^]/); const endPos = nextOpPos === -1 ? text.length : pos + nextOpPos; fragment.appendChild(document.createTextNode(text.substring(pos, endPos))); pos = endPos; } } if (fragment.childNodes.length > 1 || (fragment.childNodes.length === 1 && fragment.firstChild.nodeType === Node.ELEMENT_NODE)) { node.parentNode.replaceChild(fragment, node); } } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.className && ( node.className.includes('hljs-string') || node.className.includes('hljs-comment') || node.className.includes('hljs-number') || node.className.includes('hljs-operator-enhanced') || node.className.includes('bracket-level') )) return; Array.from(node.childNodes).forEach(processNode); } } processNode(element); } function applyVariableHighlight(element) { const patterns = { cpp: /\b(?:int|long|float|double|char|bool|string|auto|void)\s+(\w+)/g, c: /\b(?:int|long|float|double|char|void)\s+(\w+)/g, python: /\b(\w+)\s*=/g, java: /\b(?:int|long|float|double|char|boolean|String)\s+(\w+)/g, javascript: /\b(?:var|let|const)\s+(\w+)/g, go: /(\w+)\s*:=/g }; const pattern = patterns[currentLang]; if (!pattern) return; const fullText = element.textContent; const varNames = new Set(); pattern.lastIndex = 0; let match; while ((match = pattern.exec(fullText)) !== null) { if (match[1] && !/^(true|false|null|NULL|nullptr)$/.test(match[1])) { varNames.add(match[1]); } } if (varNames.size === 0) return; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; const matches = []; varNames.forEach(varName => { const regex = new RegExp(`\\b${varName}\\b`, 'g'); let m; while ((m = regex.exec(text)) !== null) { matches.push({ start: m.index, end: m.index + varName.length, text: varName }); } }); if (matches.length === 0) return; matches.sort((a, b) => a.start - b.start); const filtered = []; let lastEnd = -1; matches.forEach(m => { if (m.start >= lastEnd) { filtered.push(m); lastEnd = m.end; } }); if (filtered.length === 0) return; const fragment = document.createDocumentFragment(); let pos = 0; filtered.forEach(m => { if (m.start > pos) { fragment.appendChild(document.createTextNode(text.substring(pos, m.start))); } const span = document.createElement('span'); span.className = 'hljs-variable-enhanced'; span.textContent = m.text; fragment.appendChild(span); pos = m.end; }); if (pos < text.length) { fragment.appendChild(document.createTextNode(text.substring(pos))); } node.parentNode.replaceChild(fragment, node); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.className && ( node.className.includes('hljs-keyword') || node.className.includes('hljs-string') || node.className.includes('hljs-comment') || node.className.includes('hljs-variable-enhanced') || node.className.includes('hljs-type-enhanced') || node.className.includes('bracket-level') )) return; Array.from(node.childNodes).forEach(processNode); } } processNode(element); } function updateLineNumbers() { const editor = document.getElementById('uojCodeEditor'); const lineNumbers = document.getElementById('uojLineNumbers'); if (!editor || !lineNumbers) return; const lines = editor.value.split('\n').length; const lineHtml = Array(lines).fill(0).map((_, i) => `
${i + 1}
` ).join(''); lineNumbers.innerHTML = lineHtml; } function syncScroll() { const editor = document.getElementById('uojCodeEditor'); const highlightPre = document.querySelector('.uoj-code-highlight'); const lineNumbers = document.getElementById('uojLineNumbers'); if (editor && highlightPre && lineNumbers) { highlightPre.scrollTop = editor.scrollTop; highlightPre.scrollLeft = editor.scrollLeft; lineNumbers.scrollTop = editor.scrollTop; } } function updateCursorInfo() { const editor = document.getElementById('uojCodeEditor'); const info = document.getElementById('uojEditorInfo'); if (!editor || !info) return; const value = editor.value; const pos = editor.selectionStart; const lines = value.substring(0, pos).split('\n'); const line = lines.length; const col = lines[lines.length - 1].length + 1; info.textContent = `行 ${line}, 列 ${col} | ${langNames[currentLang]}`; } function handleKey(e) { const editor = document.getElementById('uojCodeEditor'); if (!editor) return; const start = editor.selectionStart; const end = editor.selectionEnd; const value = editor.value; const before = value.substring(0, start); const after = value.substring(end); const pairs = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '<': '>' }; const closingChars = [')', ']', '}', '"', "'", '>']; if (closingChars.includes(e.key)) { const nextChar = value.charAt(start); if (nextChar === e.key) { e.preventDefault(); editor.selectionStart = editor.selectionEnd = start + 1; return; } } if (pairs[e.key]) { e.preventDefault(); const pair = pairs[e.key]; if (start !== end) { const selectedText = value.substring(start, end); editor.value = before + e.key + selectedText + pair + after; editor.selectionStart = start + 1; editor.selectionEnd = start + 1 + selectedText.length; } else { editor.value = before + e.key + pair + after; editor.selectionStart = editor.selectionEnd = start + 1; } updateCode(); return; } if (e.key === 'Enter') { e.preventDefault(); const lines = before.split('\n'); const currentLine = lines[lines.length - 1]; const indentMatch = currentLine.match(/^\s*/); const indent = indentMatch ? indentMatch[0] : ""; let extraIndent = ""; let closingBrace = ""; const charBefore = value.charAt(start - 1); const charAfter = value.charAt(start); const isInBrackets = (charBefore === '{' && charAfter === '}') || (charBefore === '[' && charAfter === ']') || (charBefore === '(' && charAfter === ')'); if (isInBrackets) { extraIndent = " "; closingBrace = "\n" + indent; } else if (currentLine.trim().endsWith('{')) { extraIndent = " "; } const insertText = "\n" + indent + extraIndent; editor.value = before + insertText + closingBrace + after; editor.selectionStart = editor.selectionEnd = start + insertText.length; updateCode(); return; } if (e.key === 'Backspace' && start === end) { const charBefore = value.charAt(start - 1); const charAfter = value.charAt(start); if (pairs[charBefore] === charAfter) { e.preventDefault(); editor.value = value.substring(0, start - 1) + value.substring(start + 1); editor.selectionStart = editor.selectionEnd = start - 1; updateCode(); return; } } if (e.key === 'Tab') { e.preventDefault(); if (start !== end) { const selectedText = value.substring(start, end); const lines = selectedText.split('\n'); if (e.shiftKey) { const unindented = lines.map(line => { if (line.startsWith(' ')) return line.substring(4); if (line.startsWith('\t')) return line.substring(1); return line; }).join('\n'); editor.value = before + unindented + after; editor.selectionStart = start; editor.selectionEnd = start + unindented.length; } else { const indented = lines.map(line => ' ' + line).join('\n'); editor.value = before + indented + after; editor.selectionStart = start; editor.selectionEnd = start + indented.length; } } else { editor.value = before + " " + after; editor.selectionStart = editor.selectionEnd = start + 4; } updateCode(); return; } if (e.key === '/' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); const lines = value.split('\n'); const startLine = value.substring(0, start).split('\n').length - 1; const endLine = value.substring(0, end).split('\n').length - 1; const commentSymbol = currentLang === 'python' ? '#' : '//'; let allCommented = true; for (let i = startLine; i <= endLine; i++) { if (!lines[i].trim().startsWith(commentSymbol)) { allCommented = false; break; } } for (let i = startLine; i <= endLine; i++) { if (allCommented) { lines[i] = lines[i].replace(new RegExp(`^(\\s*)${commentSymbol}\\s?`), '$1'); } else { const indent = lines[i].match(/^\s*/)[0]; lines[i] = indent + commentSymbol + ' ' + lines[i].substring(indent.length); } } editor.value = lines.join('\n'); updateCode(); return; } setTimeout(updateCursorInfo, 0); } function addOpenButton() { if (document.getElementById('uojOpenBtn')) { console.log('[Universal OJ] 按钮已存在'); return true; } if (currentPlatform.key === 'nowcoder') { const containers = [ document.querySelector('.question-operate'), document.querySelector('.terminal-topic-operation'), document.querySelector('.terminal-topic') ]; for (const container of containers) { if (container) { console.log('[Universal OJ] 找到牛客按钮容器:', container); const btn = document.createElement('button'); btn.id = 'uojOpenBtn'; btn.style.cssText = ` display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid #52c41a; border-radius: 4px; background: transparent; color: #52c41a; font-size: 14px; cursor: pointer; margin-left: 10px; transition: all 0.3s; `; btn.innerHTML = ` 高级编辑器 `; btn.onmouseover = () => { btn.style.background = '#52c41a'; btn.style.color = '#fff'; }; btn.onmouseout = () => { btn.style.background = 'transparent'; btn.style.color = '#52c41a'; }; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openEditor(); }); container.appendChild(btn); console.log('[Universal OJ] 牛客按钮添加成功'); return true; } } } if (currentPlatform.key === 'luogu') { const containers = [ document.querySelector('.ide-toolbar .actions'), document.querySelector('.problem .actions'), document.querySelector('a[class*="title"]:has(.fa-paper-plane)')?.parentElement ]; for (const container of containers) { if (container) { console.log('[Universal OJ] 找到按钮容器:', container); const btn = document.createElement('a'); btn.id = 'uojOpenBtn'; btn.className = 'uoj-open-btn-luogu title'; btn.href = 'javascript:void(0)'; btn.innerHTML = ` 高级编辑器 `; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openEditor(); }); container.appendChild(btn); console.log('[Universal OJ] 按钮添加成功'); return true; } } } if (currentPlatform.key === 'codeforces') { const containers = [ document.querySelector('.second-level-menu'), document.querySelector('.sidebox .rtable'), document.querySelector('.problem-statement .header'), document.querySelector('.roundbox.menu-box') ]; for (const container of containers) { if (container) { const btn = document.createElement('div'); btn.style.cssText = "margin: 10px 0; display: inline-block; vertical-align: middle;"; btn.innerHTML = ` 高级编辑器 `; const link = btn.querySelector('a'); link.onmouseover = () => { link.style.background = '#FF591F'; link.style.color = '#fff'; }; link.onmouseout = () => { link.style.background = '#fff'; link.style.color = '#FF591F'; }; link.onclick = (e) => { e.preventDefault(); openEditor(); }; if (container.tagName === 'UL') { const li = document.createElement('li'); li.appendChild(btn); container.appendChild(li); } else { container.appendChild(btn); } return true; } } } console.log('[Universal OJ] 未找到合适的按钮容器'); return false; } function init() { console.log('[Universal OJ] 开始初始化...'); setTimeout(() => { if (addOpenButton()) return; let attempts = 0; const interval = setInterval(() => { console.log(`[Universal OJ] 尝试添加按钮 (${attempts + 1}/20)`); if (addOpenButton() || attempts >= 20) { clearInterval(interval); if (attempts >= 20) { console.error('[Universal OJ] 添加按钮失败,已达最大尝试次数'); } } attempts++; }, 1000); }, 1000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();