// ==UserScript== // @name 超星复制粘贴助手 (Chaoxing Copy&Paste Helper) // @namespace https://github.com/iPycc/Chaoxing-Copy&Paste-Helper // @version 2.2 // @description 超星网页端字体解密、强制启用粘贴功能、一键复制所有题目,支持复制题目、UEditor富文本编辑器和所有输入元素 // @author iPycc & wyn665817 // @match *://*.chaoxing.com/* // @require https://scriptcat.org/lib/668/1.0/TyprMd5.js // @resource Table https://www.forestpolice.org/ttf/2.0/table.json // @run-at document-start // @grant unsafeWindow // @grant GM_getResourceText // @license MIT // ==/UserScript== (function() { 'use strict'; const ChaoxingHelper = { // 复制 (字体解密及替换) copyEnabler: { decrypt() { const styleElement = this.findStyleContaining('font-cxsecret'); if (!styleElement) return; const fontMatch = styleElement.textContent.match(/base64,([\w\W]+?)'/); if (!fontMatch) return; const fontData = Typr.parse(this.base64ToUint8Array(fontMatch[1]))[0]; const table = JSON.parse(GM_getResourceText('Table')); const charMap = this.createCharMap(fontData, table); this.replaceEncryptedText(charMap); console.log(`[Chaoxing Helper] Decryption completed ${Object.keys(charMap).length} characters`); }, findStyleContaining(text) { const styles = document.querySelectorAll('style'); return Array.from(styles).find(style => style.textContent.includes(text) ); }, base64ToUint8Array(base64) { const data = window.atob(base64); return new Uint8Array([...data].map(char => char.charCodeAt(0))); }, createCharMap(font, table) { const charMap = {}; for (let i = 19968; i < 40870; i++) { const glyph = Typr.U.codeToGlyph(font, i); if (!glyph) continue; const path = Typr.U.glyphToPath(font, glyph); const pathHash = md5(JSON.stringify(path)).slice(24); if (table[pathHash]) { charMap[String.fromCharCode(i)] = String.fromCharCode(table[pathHash]); } } return charMap; }, replaceEncryptedText(charMap) { const elements = document.querySelectorAll('.font-cxsecret'); elements.forEach(element => { let html = element.innerHTML; Object.entries(charMap).forEach(([encrypted, decrypted]) => { html = html.replace(new RegExp(encrypted, 'g'), decrypted); }); element.innerHTML = html; element.classList.remove('font-cxsecret'); }); } }, // 粘贴 (强制启用粘贴功能) pasteEnabler: { init() { this.removeGlobalRestrictions(); this.injectGlobalStyles(); this.enableExistingElements(); this.startMutationObserver(); this.monitorUEditor(); }, removeGlobalRestrictions() { ['paste', 'contextmenu', 'selectstart', 'dragstart', 'copy', 'cut', 'keydown'] .forEach(event => document[`on${event}`] = null); this.removeElementRestrictions(document.body); }, injectGlobalStyles() { const style = document.createElement('style'); style.textContent = ` * { -webkit-user-select: text !important; -moz-user-select: text !important; -ms-user-select: text !important; user-select: text !important; } input, textarea, [contenteditable] { user-select: text !important; pointer-events: auto !important; } `; document.head.appendChild(style); }, removeElementRestrictions(element) { if (!element) return; // 移除事件监听器和属性 ['paste', 'contextmenu', 'keydown', 'selectstart', 'dragstart', 'copy', 'cut'] .forEach(event => { element[`on${event}`] = null; element.removeAttribute(`on${event}`); }); element.removeAttribute('readonly'); element.removeAttribute('disabled'); // 恢复样式 Object.assign(element.style, { userSelect: 'text', webkitUserSelect: 'text', mozUserSelect: 'text', msUserSelect: 'text', pointerEvents: 'auto' }); }, enableExistingElements() { document.querySelectorAll('input, textarea, [contenteditable]') .forEach(el => this.removeElementRestrictions(el)); }, startMutationObserver() { new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; if (node.matches?.('input, textarea, [contenteditable]')) { this.removeElementRestrictions(node); } if (node.tagName === 'IFRAME') { setTimeout(() => { try { node.contentDocument?.querySelectorAll('input, textarea, [contenteditable]') .forEach(el => this.removeElementRestrictions(el)); } catch (e) { // 跨域iframe,静默处理 } }, 500); } node.querySelectorAll?.('input, textarea, [contenteditable]') .forEach(el => this.removeElementRestrictions(el)); }); }); }).observe(document.body, { childList: true, subtree: true }); }, handlePaste(e) { e.preventDefault(); e.stopPropagation(); const doc = e.target.ownerDocument || document; const clipboardData = e.clipboardData || window.clipboardData; // 处理图片粘贴 const imageItem = Array.from(clipboardData?.items || []) .find(item => item.type?.startsWith('image/')); if (imageItem) { const imageFile = imageItem.getAsFile(); const reader = new FileReader(); reader.onload = () => this.insertImage(doc, reader.result); reader.readAsDataURL(imageFile); return; } // 处理文本粘贴 const text = clipboardData?.getData?.('text/plain') || window.clipboardData?.getData?.('Text') || ''; if (text) { this.insertText(doc, text); } }, insertImage(doc, dataUrl) { const img = doc.createElement('img'); img.src = dataUrl; img.alt = '粘贴图片'; const selection = doc.getSelection(); if (selection?.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); range.insertNode(img); range.setStartAfter(img); selection.removeAllRanges(); selection.addRange(range); } else { doc.body?.appendChild(img); } }, insertText(doc, text) { if (doc.execCommand?.('insertText', false, text)) return true; const selection = doc.getSelection(); if (selection?.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); const textNode = doc.createTextNode(text); range.insertNode(textNode); range.setStartAfter(textNode); selection.removeAllRanges(); selection.addRange(range); return true; } doc.body?.appendChild(doc.createTextNode(text)); return false; }, monitorUEditor() { let attempts = 0; const maxAttempts = 20; const checkUEditor = () => { attempts++; // 处理主窗口UEditor实例 if (typeof UE !== 'undefined' && UE.instants) { Object.values(UE.instants).forEach(instance => { this.processUEditorInstance(instance); }); } // 处理iframe中的UEditor实例 document.querySelectorAll('iframe').forEach(iframe => { try { const iframeWindow = iframe.contentWindow; if (iframeWindow?.UE?.instants) { Object.values(iframeWindow.UE.instants).forEach(instance => { this.processUEditorInstance(instance); }); } } catch (error) { // 跨域iframe,静默处理 } }); if (attempts < maxAttempts) setTimeout(checkUEditor, 500); }; checkUEditor(); }, processUEditorInstance(instance) { if (!instance || typeof instance !== 'object') return; // 启用编辑器 if (instance.options) { instance.options.pasteplain = true; instance.options.readonly = false; instance.options.disabled = false; } if (instance.setEnabled && instance.body) { instance.setEnabled(true); } // 处理编辑器主体 if (instance.body && !instance.body.__cxPasteEnabled) { this.removeElementRestrictions(instance.body); instance.body.addEventListener('paste', (e) => this.handlePaste(e), true); instance.body.__cxPasteEnabled = true; } // 处理iframe文档 const iframeDoc = instance.iframe?.contentDocument; if (iframeDoc && !iframeDoc.__cxPasteEnabled) { this.removeElementRestrictions(iframeDoc.body); iframeDoc.addEventListener('paste', (e) => this.handlePaste(e), true); iframeDoc.__cxPasteEnabled = true; } } }, // 一键复制所有题目 copyAllQuestion: { MESSAGE: { REQUEST: 'CX_GET_TITLES', RESPONSE: 'CX_TITLES_RESPONSE' }, init() { // 跨页面消息监听(子 iframe 也会加载本脚本) this.setupMessageHandler(); // 顶层页面插入复制按钮 this.insertCopyButton(); // 注入模态框 this.injectModal(); }, // 注入模态框的HTML和CSS injectModal() { const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); // 获取模态框元素 const modalOverlay = document.getElementById('cx-copy-modal-overlay'); const textarea = document.getElementById('cx-copy-modal-textarea'); const cancelButton = document.getElementById('cx-copy-modal-cancel'); const copyButton = document.getElementById('cx-copy-modal-copy'); // 取消按钮事件 cancelButton.addEventListener('click', () => { modalOverlay.style.display = 'none'; }); // 复制按钮事件 copyButton.addEventListener('click', async () => { try { await this.copyToClipboard(textarea.value); this.toast('已复制题目内容'); modalOverlay.style.display = 'none'; } catch (e) { this.toast('复制失败,请重试'); } }); // 点击遮罩层关闭模态框 modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { modalOverlay.style.display = 'none'; } }); }, // 显示模态框并填充内容 showModal(content) { const modalOverlay = document.getElementById('cx-copy-modal-overlay'); const textarea = document.getElementById('cx-copy-modal-textarea'); if (modalOverlay && textarea) { textarea.value = content; modalOverlay.style.display = 'flex'; } }, setupMessageHandler() { window.addEventListener('message', (event) => { try { const data = event.data || {}; if (data?.type !== this.MESSAGE.REQUEST || !data.id) return; // 只响应超星域名消息 const host = new URL(event.origin).hostname || ''; if (!/chaoxing\.com$/.test(host)) return; const titles = this.extractTitlesFromDocument(document); // 兼容旧协议 const questions = this.extractQuestionsFromDocument(document); const header = this.findHeaderTitle(document) || document.title || ''; event.source?.postMessage({ type: this.MESSAGE.RESPONSE, id: data.id, titles, // 兼容旧协议 questions, header }, event.origin); } catch (err) { // 忽略跨域或解析错误 } }, false); }, insertCopyButton() { const h2s = Array.from(document.querySelectorAll('h2')); const anchor = h2s.find(h => h.textContent?.trim()?.includes('章节详情')); if (!anchor) return; if (document.getElementById('__cx_copy_all_btn')) return; const btn = document.createElement('button'); btn.id = '__cx_copy_all_btn'; btn.textContent = '点击复制所有题目'; btn.setAttribute('type', 'button'); btn.style.cssText = [ 'position: sticky', 'top: 8px', 'margin: 8px 0', 'padding: 6px 12px', 'background: #1677ff', 'color: #fff', 'border: none', 'border-radius: 4px', 'font-size: 14px', 'cursor: pointer', 'z-index: 9999' ].join(';'); btn.addEventListener('click', async () => { try { const content = await this.collectAllTitles(); this.showModal(content); // 显示模态框而不是直接复制 } catch (e) { this.toast('收集题目失败,请重试'); } }); anchor.insertAdjacentElement('afterend', btn); }, // 汇总顶层及所有 iframe 的题目 async collectAllTitles() { const sections = await this.collectTitlesFromDocument(document); const lines = []; sections.forEach(({ header, questions }) => { if (questions?.length) { if (header) { lines.push(`# ${header}`); lines.push(''); } questions.forEach((q, idx) => { const titleLine = `${idx + 1}. ${q.title}`; lines.push(titleLine); if (Array.isArray(q.options) && q.options.length) { q.options.forEach(opt => lines.push(opt)); } lines.push(''); }); } }); return lines.join('\n'); }, // 递归采集指定文档内所有题目(含嵌套 iframe) async collectTitlesFromDocument(doc) { const sections = []; const header = this.findHeaderTitle(doc) || doc.title || ''; const questions = this.extractQuestionsFromDocument(doc); if (questions.length) sections.push({ header, questions }); const frames = Array.from(doc.querySelectorAll('iframe')); for (const frame of frames) { try { const fdoc = frame.contentDocument; if (fdoc) { const childSections = await this.collectTitlesFromDocument(fdoc); sections.push(...childSections); } else { const res = await this.collectFromFrame(frame); if (res?.questions?.length) sections.push(res); } } catch (_) { const res = await this.collectFromFrame(frame); if (res?.questions?.length) sections.push(res); } } return sections; }, collectFromFrame(frame) { // 优先同源直接采集 try { const doc = frame.contentDocument; if (doc) { const questions = this.extractQuestionsFromDocument(doc); const header = this.findHeaderTitle(doc) || doc.title || ''; return Promise.resolve({ header, questions }); } } catch (_) { // frame } // 跨域使用 postMessage 请求 const id = Math.random().toString(36).slice(2); return new Promise((resolve) => { const onMessage = (event) => { const data = event.data || {}; if (data?.type === this.MESSAGE.RESPONSE && data.id === id) { window.removeEventListener('message', onMessage); const questions = Array.isArray(data.questions) ? data.questions : (data.titles || []).map(t => ({ title: t, options: [], type: 'other' })); resolve({ header: data.header || '', questions }); } }; window.addEventListener('message', onMessage); // 超时保护 const timer = setTimeout(() => { window.removeEventListener('message', onMessage); resolve({ header: '', titles: [] }); }, 3000); try { frame.contentWindow?.postMessage({ type: this.MESSAGE.REQUEST, id }, '*'); } catch (e) { clearTimeout(timer); window.removeEventListener('message', onMessage); resolve({ header: '', titles: [] }); } }); }, extractTitlesFromDocument(doc) { // 覆盖更多页面结构(老版/新版、题目容器、TiMu 包裹等) const primarySelectors = [ '.Zy_TItle .fontLabel', '.Zy_Title .fontLabel', '.newZy_TItle .fontLabel', '.newZy_Title .fontLabel', '.TiMu .Zy_TItle .fontLabel', '.TiMu .Zy_Title .fontLabel', '[class*="Zy_"][class*="TItle"] .fontLabel', '[class*="Zy_"][class*="Title"] .fontLabel' ]; const fallbackSelectors = [ '.Zy_TItle', '.Zy_Title', '.newZy_TItle', '.newZy_Title', '.TiMu .Zy_TItle', '.TiMu .Zy_Title' ]; const set = new Set(); // 先从 fontLabel(解密字体)中提取 primarySelectors.forEach(sel => { doc.querySelectorAll(sel).forEach(n => { const text = (n.textContent || '').replace(/\s+/g, ' ').trim(); if (text) set.add(text); }); }); // 若未命中,则退回到题目容器本身文本 if (set.size === 0) { fallbackSelectors.forEach(sel => { doc.querySelectorAll(sel).forEach(n => { const text = (n.textContent || '').replace(/\s+/g, ' ').trim(); if (text) set.add(text); }); }); } return Array.from(set); }, // 提取选择题与简答题为结构化数据 extractQuestionsFromDocument(doc) { const questions = []; const containers = Array.from(doc.querySelectorAll('.TiMu')); const getTitleFrom = (root) => { const titleNode = root.querySelector('.Zy_TItle .fontLabel, .Zy_Title .fontLabel, .newZy_TItle .fontLabel, .newZy_Title .fontLabel') || root.querySelector('.Zy_TItle, .Zy_Title, .newZy_TItle, .newZy_Title'); return (titleNode?.textContent || '').replace(/\s+/g, ' ').trim(); }; if (containers.length > 0) { containers.forEach(box => { const title = getTitleFrom(box); if (!title) return; const ul = box.querySelector('ul.Zy_ulTop.w-top.fl'); const lis = ul ? Array.from(ul.querySelectorAll(':scope > li')) : []; const options = lis.map((li, idx) => { const p = li.querySelector('p') || li.querySelector('a') || li; const text = (p?.textContent || '').replace(/\s+/g, ' ').trim(); const letter = String.fromCharCode(65 + idx); // A,B,C... return `${letter}. ${text}`; }).filter(Boolean); let type = 'other'; if (options.length === 4) type = 'single'; else if (options.length >= 5) type = 'multi'; questions.push({ title, options, type }); }); } // 若无 TiMu 容器,退回到标题列表作为简答题/其他题 if (questions.length === 0) { const titles = this.extractTitlesFromDocument(doc); titles.forEach(t => questions.push({ title: t, options: [], type: 'other' })); } return questions; }, findHeaderTitle(doc) { // 子页面:作业标题常在 .ceyan_name h3 或 .newTestTitle const h3 = doc.querySelector('.ceyan_name h3'); if (h3?.textContent) return h3.textContent.trim(); const testName = doc.querySelector('.newTestTitle .TestTitle_name'); if (testName?.textContent) return testName.textContent.trim(); // 顶层列表页面:寻找“章节详情” const h2 = Array.from(doc.querySelectorAll('h2')).find(h => h.textContent?.includes('章节详情')); if (h2?.textContent) return h2.textContent.trim(); return ''; }, async copyToClipboard(text) { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } } catch (_) { //copy not success } const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (_) { // copy } document.body.removeChild(ta); return true; }, toast(msg) { try { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = [ 'position: fixed', 'left: 50%','top: 24px','transform: translateX(-50%)', 'background: rgba(0,0,0,0.75)', 'color: #fff','padding: 8px 12px','border-radius: 4px', 'font-size: 13px','z-index: 10000' ].join(';'); document.body.appendChild(el); setTimeout(() => el.remove(), 1500); } catch (_) { // msg } } }, // 主初始化 init() { // console.log('[Chaoxing Helper] 开始初始化'); this.pasteEnabler.init(); this.copyEnabler.decrypt(); this.copyAllQuestion.init(); console.log('[Chaoxing Helper] Done'); } }; // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ChaoxingHelper.init()); } else { ChaoxingHelper.init(); } })();