您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
textarea#input の内容を AtCoder custom_test に送信。responseText を安全に扱う修正版。
// ==UserScript== // @name AHC Visualizer → AtCoder CustomTest Runner (fixed responseText guards) // @namespace idk // @version 1.2 // @description textarea#input の内容を AtCoder custom_test に送信。responseText を安全に扱う修正版。 // @match https://img.atcoder.jp/ahc* // @match https://atcoder.jp/contests/*/custom_test // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_openInTab // @grant unsafeWindow // @connect atcoder.jp // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const sleep = ms => new Promise(r => setTimeout(r, ms)); const ENC = encodeURIComponent; const TOKEN_KEY = 'ahc_csrf_token_v1'; const CONTEST_KEY = 'ahc_contest_slug_v1'; // save token/contest when on atcoder custom_test page if (location.host === 'atcoder.jp' && /\/contests\/[^/]+\/custom_test/.test(location.pathname)) { try { let token = (typeof unsafeWindow !== 'undefined' && unsafeWindow.csrfToken) ? unsafeWindow.csrfToken : null; if (!token) { const el = document.querySelector('input[name="csrf_token"]'); if (el) token = el.value; } const contest = location.pathname.split('/')[2] || null; if (token) GM_setValue(TOKEN_KEY, token); if (contest) GM_setValue(CONTEST_KEY, contest); console.log('AHC Runner: saved token/contest', { token: !!token, contest }); } catch (e) { console.warn('AHC Runner: failed to save token', e); } return; } GM_addStyle(` #ahc-mini { position: fixed; right:12px; top:12px; width:420px; max-width:86vw; z-index:2147483647; background:#fff; border:1px solid #ddd; padding:10px; border-radius:8px; box-shadow:0 6px 18px rgba(0,0,0,0.12);} #ahc-mini textarea{ width:100%; box-sizing:border-box; font-family:monospace; font-size:12px; resize:vertical;} #ahc-mini .status{ font-size:12px; margin-top:6px; color:#333; word-break:break-word;} `); const root = document.createElement('div'); root.id = 'ahc-mini'; // 1. root.innerHTML は一旦言語selectの中身だけ空にしておく root.innerHTML = ` <div style="font-weight:700;margin-bottom:6px">AHC Visualizer → AtCoder Runner</div> <div> <label style="font-size:12px">Language:</label> <select id="ahc-lang" style="margin-left:6px;"> <option>Loading...</option> </select> </div> <textarea id="ahc-code" placeholder="Paste your source code here (required)" rows="8"></textarea> <div style="margin-top:6px;"> <button id="ahc-run" style="font-size:110%">Run</button> <button id="ahc-open" style="margin-left:6px">Open Code Test</button> </div> <div class="status" id="ahc-status">Initializing...</div> `; // 2. 言語リストを取得してセレクトを更新する関数 async function updateLanguageSelect(contest) { const langSel = document.getElementById('ahc-lang'); langSel.innerHTML = ''; // 一旦クリア const langs = await fetchAllowedLanguages(contest); if (!langs || Object.keys(langs).length === 0) { langSel.innerHTML = '<option>Failed to get languages</option>'; return; } for (const [id, name] of Object.entries(langs)) { const option = document.createElement('option'); option.value = id; option.textContent = name + ' — ' + id; langSel.appendChild(option); } } // 3. スクリプト初期化時に contest を推測 or 保存値から取り出し言語を読み込む (async () => { const t = await GM_getValue(TOKEN_KEY); let c = await GM_getValue(CONTEST_KEY) || guessContestFromImg(); if (!c) { statusEl.textContent = 'Cannot guess contest. Please open custom_test page once.'; return; } await updateLanguageSelect(c); if (t && c) statusEl.textContent = `Token acquired (contest=${c}). Contents of #input will be sent as stdin.`; else statusEl.textContent = 'Token not acquired: Please open custom_test page once (auto open possible).'; })(); document.body.appendChild(root); const statusEl = document.getElementById('ahc-status'); const runBtn = document.getElementById('ahc-run'); const openBtn = document.getElementById('ahc-open'); const codeTa = document.getElementById('ahc-code'); const langSel = document.getElementById('ahc-lang'); function guessContestFromImg() { const m = location.href.match(/img\.atcoder\.jp\/([^\/?#/]+)/); return m ? m[1] : null; } async function ensureTokenAndContest(timeout = 30000) { let token = await GM_getValue(TOKEN_KEY); let contest = await GM_getValue(CONTEST_KEY); if (!contest) { const g = guessContestFromImg(); if (g) { contest = g; await GM_setValue(CONTEST_KEY, contest); } } if (token && contest) return { token, contest }; const guessed = contest || guessContestFromImg(); if (!guessed) { statusEl.textContent = 'Cannot guess contest. Check URL.'; throw new Error('contest slug not found'); } statusEl.textContent = 'Token not acquired: Opening custom_test in new tab (manual ok)'; try { GM_openInTab(`https://atcoder.jp/contests/${guessed}/custom_test`, { active:false, insert:true }); } catch (e) { statusEl.textContent = 'Auto open blocked. Please open manually.'; } const start = Date.now(); while (Date.now() - start < timeout) { token = await GM_getValue(TOKEN_KEY); contest = await GM_getValue(CONTEST_KEY); if (token && contest) { statusEl.textContent = 'Token acquired. Ready to run.'; return { token, contest }; } await sleep(400); } statusEl.textContent = 'Token acquisition timed out. Please open custom_test page once.'; throw new Error('token timeout'); } async function fetchAllowedLanguages(contest) { const url = `https://atcoder.jp/contests/${contest}/custom_test`; return await new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url, onload: res => { try { const respText = String(res.responseText ?? ""); const doc = new DOMParser().parseFromString(respText, 'text/html'); const sel = doc.querySelector('select[name="data.LanguageId"], select#language, select[name="LanguageId"]'); if (!sel) return resolve({}); const opts = Array.from(sel.options).map(o => ({ id: o.value, text: o.textContent.trim() })); const map = {}; opts.forEach(o => { if (o.id) map[o.id] = o.text; }); resolve(map); } catch (e) { resolve({}); } }, onerror: () => resolve({}) }); }); } function findOutputElem() { return document.getElementById('output') || document.querySelector('textarea[name="output"], textarea#output, textarea.output') || document.querySelector('[contenteditable="true"], pre, code'); } async function submitAndPoll({contest, token, languageId, sourceCode, inputText}) { const allowed = await fetchAllowedLanguages(contest); console.log('allowed languages:', allowed); if (allowed && Object.keys(allowed).length > 0 && !allowed[languageId]) { const firstId = Object.keys(allowed)[0]; statusEl.textContent = `Selected language not allowed. Automatically switching to ${allowed[firstId]} (${firstId}).`; languageId = firstId; langSel.value = languageId; } const submitUrl = `https://atcoder.jp/contests/${contest}/custom_test/submit/json`; const resultUrl = `https://atcoder.jp/contests/${contest}/custom_test/json?reload=true`; const body = `data.LanguageId=${ENC(languageId)}&sourceCode=${ENC(sourceCode)}&input=${ENC(inputText)}&csrf_token=${ENC(token)}`; statusEl.textContent = 'Sending...'; const postPreview = String(body ?? "").slice(0, 1200); console.log('[AHC Runner] POST body preview', postPreview); await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: submitUrl, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, data: body, onload: r => { const resp = String(r.responseText ?? ""); console.log('[AHC Runner] POST response', r.status, resp.slice(0, 400)); if (r.status >= 400) return reject(new Error('submit HTTP ' + r.status + ': ' + resp)); if (resp && /言語.*許可/.test(resp)) return reject(new Error(resp)); resolve(r); }, onerror: e => reject(new Error('submit failed: ' + e)) }); }); statusEl.textContent = 'Running (polling)...'; const start = Date.now(); const TIMEOUT = 2 * 60 * 1000; while (true) { if (Date.now() - start > TIMEOUT) throw new Error('Result retrieval timeout'); const data = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: resultUrl, onload: r => { const resp = String(r.responseText ?? ""); try { resolve(JSON.parse(resp)); } catch (e) { resolve({}); } }, onerror: e => reject(e) }); }); console.log('[AHC Runner] poll:', data); if (data && ('Interval' in data)) { await sleep(Math.max(200, Number(data.Interval) || 500)); continue; } if (!data || !('Result' in data)) { await sleep(300); continue; } return data; } } runBtn.addEventListener('click', async () => { runBtn.disabled = true; statusEl.textContent = 'Preparing...'; try { const { token, contest } = await ensureTokenAndContest(30000); let languageId = String(langSel.value || '4003'); const sourceCode = codeTa.value || ''; if (!sourceCode.trim()) { alert('Please input source code'); runBtn.disabled = false; return; } const inputElem = document.getElementById('input'); if (!inputElem) { alert('textarea#input not found'); runBtn.disabled = false; return; } await sleep(80); const inputText = inputElem.value || ''; console.log('[AHC Runner] inputText preview:', String(inputText).slice(0, 800)); if (!inputText && !confirm('textarea#input is empty. Run with empty input?')) { runBtn.disabled = false; return; } const result = await submitAndPoll({ contest, token, languageId, sourceCode, inputText }); const stdout = (typeof result.Stdout !== 'undefined') ? String(result.Stdout) : ''; const stderr = (typeof result.Stderr !== 'undefined') ? String(result.Stderr) : ''; const exit = result.Result && result.Result.ExitCode; const time = result.Result && result.Result.TimeConsumption; const outEl = findOutputElem(); if (outEl) { if ('value' in outEl) { outEl.value = stdout; outEl.dispatchEvent(new Event('input', { bubbles: true })); } else { outEl.textContent = stdout; } } else { console.log('[AHC Runner] stdout:', stdout.slice(0, 800)); } statusEl.textContent = `Finished: exit=${exit} time=${time}ms stderr=${stderr ? 'present' : 'none'}`; } catch (err) { console.error('[AHC Runner] error', err); statusEl.textContent = 'Error: ' + (err && err.message ? err.message : err); alert('An error occurred during execution. Check the console.'); } finally { runBtn.disabled = false; } }); openBtn.addEventListener('click', () => { const g = guessContestFromImg(); if (!g) { alert('Cannot guess contest'); return; } window.open(`https://atcoder.jp/contests/${g}/custom_test`, '_blank'); }); (async () => { const t = await GM_getValue(TOKEN_KEY); const c = await GM_getValue(CONTEST_KEY) || guessContestFromImg(); if (t && c) statusEl.textContent = `Token acquired (contest=${c}). Contents of #input will be sent as stdin.`; else statusEl.textContent = 'Token not acquired: Please open custom_test page once (auto open possible).'; })(); function addCopyButton(textarea) { if (!textarea) return; const label = document.querySelector(`label[for="${textarea.id}"]`); // ラベルとボタンを同じ行に配置 const header = document.createElement('div'); header.style.display = 'flex'; header.style.alignItems = 'center'; header.style.gap = '6px'; // ラベルとボタンの間隔 if (label) { textarea.parentNode.insertBefore(header, label); header.appendChild(label); } else { textarea.parentNode.insertBefore(header, textarea); } const btn = document.createElement('button'); btn.textContent = 'Copy'; btn.type = 'button'; btn.style.padding = '1px 6px 1px 6px'; btn.style.margin = '1px'; btn.addEventListener('click', () => { navigator.clipboard.writeText(textarea.value).then(() => { btn.textContent = 'Copied'; setTimeout(() => { btn.textContent = 'Copy'; }, 1000); }).catch(() => { alert('failed to copy'); }); }); header.appendChild(btn); // textarea をラベル・ボタンの下に配置 header.insertAdjacentElement('afterend', textarea); } window.addEventListener('load', () => { addCopyButton(document.getElementById('input')); addCopyButton(document.getElementById('output')); }); })();