Greasy Fork is available in English.

AtCoderProblemCopier

AtCoderの問題文の横に、Markdown形式で問題文をコピーするボタンを追加します。

2026-02-11 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         AtCoderProblemCopier
// @name:en      AtCoderProblemCopier
// @name:zh-CN   AtCoderProblemCopier
// @name:zh-TW   AtCoderProblemCopier
// @namespace    https://github.com/HHayasaka25/AtCoder-Problem-Copier
// @version      0.0.1
// @description  AtCoderの問題文の横に、Markdown形式で問題文をコピーするボタンを追加します。
// @description:en Add buttons to copy AtCoder problem statements in Markdown format
// @description:zh-CN 在AtCoder题目页面添加Markdown格式的复制按钮
// @description:zh-TW 在AtCoder題目頁面添加Markdown格式的複製按鈕
// @author       LIama
// @license      MIT
// @homepage     https://github.com/HHayasaka25/AtCoder-Problem-Copier
// @supportURL   https://github.com/HHayasaka25/AtCoder-Problem-Copier/issues
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
	'use strict';

	function main() {
		initButtons();
		watchLanguageSwitch();
	}


	function initButtons() {
		console.log('[AtCoder Copy] initButtons() 開始');

		// 日本語と英語の両方の最初のh3にボタンを追加
		const jaHeader = document.querySelector(".lang-ja h3");
		const enHeader = document.querySelector(".lang-en h3");

		console.log('[AtCoder Copy] 日本語ヘッダー:', jaHeader);
		console.log('[AtCoder Copy] 英語ヘッダー:', enHeader);

		if (jaHeader && !jaHeader.querySelector(".ext-copy-group")) {
			console.log('[AtCoder Copy] 日本語ヘッダーにボタンを追加');
			const group = document.createElement("span");
			group.className = "ext-copy-group";
			group.style.marginLeft = "10px";

			group.appendChild(makeButtons("コピー", "ja"));
			group.appendChild(makeButtons("Copy", "en"));

			jaHeader.appendChild(group);
		} else {
			console.log('[AtCoder Copy] 日本語ヘッダーにはボタンが既に存在、またはヘッダーが見つからない');
		}

		if (enHeader && !enHeader.querySelector(".ext-copy-group")) {
			console.log('[AtCoder Copy] 英語ヘッダーにボタンを追加');
			const group = document.createElement("span");
			group.className = "ext-copy-group";
			group.style.marginLeft = "10px";

			group.appendChild(makeButtons("コピー", "ja"));
			group.appendChild(makeButtons("Copy", "en"));

			enHeader.appendChild(group);
		} else {
			console.log('[AtCoder Copy] 英語ヘッダーにはボタンが既に存在、またはヘッダーが見つからない');
		}

		console.log('[AtCoder Copy] initButtons() 完了');
	}

	// 言語切り替えボタンのクリックを監視
	function watchLanguageSwitch() {
		console.log('[AtCoder Copy] watchLanguageSwitch() 開始');

		const langBtn = document.querySelector('#task-lang-btn');
		console.log('[AtCoder Copy] 言語切り替えボタン:', langBtn);

		if (!langBtn) {
			console.log('[AtCoder Copy] 言語切り替えボタンが見つかりません');
			return;
		}

		// 言語切り替えボタンのクリックを監視
		langBtn.addEventListener('click', function (e) {
			console.log('[AtCoder Copy] 言語切り替えボタンがクリックされました', e.target);

			// 少し遅延させてからボタンを追加(DOMの更新を待つ)
			setTimeout(() => {
				console.log('[AtCoder Copy] 50ms後、ボタンを再追加');
				initButtons();
			}, 50);
		});

		console.log('[AtCoder Copy] 言語切り替えボタンの監視を開始しました');
	}

	// ボタンを生成する
	function makeButtons(label, lang) {
		const btn = document.createElement("span");

		// クラスを完全に一致させる
		btn.className = "btn btn-default btn-sm btn-copy ml-1";

		// 属性の設定(元のボタンと同じ構成)
		btn.setAttribute("tabindex", "0");
		btn.setAttribute("data-toggle", "tooltip");
		btn.setAttribute("data-trigger", "manual");
		btn.setAttribute("title", "");
		btn.setAttribute("data-original-title", "Copied!");

		btn.innerText = label;

		// クリックイベント
		btn.onclick = async (e) => {
			e.preventDefault();
			e.stopPropagation();

			console.log(`[AtCoder Copy] ${label}ボタンがクリックされました (lang: ${lang})`);

			const data = getTask();
			const text = (lang === "ja")
				? [data.limit, ...data.ja].join("\n\n")
				: [data.limit, ...data.en].join("\n\n");

			if (lang === "en" && data.en.length === 0) {
				console.log('[AtCoder Copy] 英語版が存在しません');
				return alert("No English");
			}

			try {
				await navigator.clipboard.writeText(text);
				console.log('[AtCoder Copy] クリップボードへのコピー成功');

				btn.blur();

				// 元のボタンと同じ挙動:ボタンの色は変えず、tooltipのみ表示
				if (typeof $ !== 'undefined' && $.fn.tooltip) {
					// tooltipを表示
					$(btn).tooltip('show');

					// 約1秒後に非表示
					setTimeout(() => {
						$(btn).tooltip('hide');
					}, 1000);
				}
			} catch (err) {
				console.error("[AtCoder Copy] コピー失敗:", err);
				alert("コピーに失敗しました");
			}
		};

		return btn;
	}

	// ソースから該当部分を取り出す
	// 実行時間制限・メモリ制限
	function getLimit() {
		const target = document.querySelector("#main-container");
		const lines = target.innerText.split("\n")
		for (const line of lines) {
			if (line.startsWith("実行時間制限")) {
				return line;
			}
		}
	}

	// 問題文部分の取り出し
	function getTask() {
		const limit = getLimit();

		const container = document.querySelector("#task-statement")
		if (!container) return { limit: limit, ja: [], en: [] };

		const elements = container.querySelectorAll(".part");
		const allParts = [];

		// HTML から Markdown に変換
		elements.forEach(element => {
			const htmlText = element.innerHTML;
			const markdown = convertToMarkdown(htmlText);
			allParts.push(markdown);
		})

		// 日本語の問題文と英語の問題文を分割
		const splitIndex = allParts.findIndex(text => text.includes("Problem Statement"));
		let japaneseParts = [];
		let englishParts = [];

		if (splitIndex === -1) {
			japaneseParts = allParts;
		} else {
			japaneseParts = allParts.slice(0, splitIndex);
			englishParts = allParts.slice(splitIndex);
		}

		return {
			limit: limit,
			ja: japaneseParts,
			en: englishParts
		}
	}

	// この関数はchatGPTに助けを請うた(諦め)
	function convertToMarkdown(htmlText) {
		const parser = new DOMParser();
		const doc = parser.parseFromString(htmlText, "text/html");

		function walk(node, inPre = false) {
			// --- TEXT ---
			if (node.nodeType === Node.TEXT_NODE) {
				if (inPre) return "";
				let text = node.textContent;
				text = text.replace(/\s+/g, " ");
				text = text.replace(/([^\x01-\x7E]) ([^\x01-\x7E])/g, "$1$2");
				return text;
			}

			if (node.nodeType !== Node.ELEMENT_NODE) return "";

			// --- 無視 ---
			if (
				node.classList.contains("ext-copy-group") ||
				node.classList.contains("div-btn-copy") ||
				node.classList.contains("katex-html")
			) {
				return "";
			}

			// --- KaTeX 数式本体 ---
			if (node.classList.contains("katex-mathml")) {
				const anno = node.querySelector(
					'annotation[encoding="application/x-tex"]'
				);
				if (!anno) return "";
				const latex = anno.textContent.trim();
				return inPre ? `$$${latex}$$` : `$${latex}$`;
			}

			const tag = node.tagName;

			// --- PRE ---
			if (tag === "PRE") {
				const formulas = node.querySelectorAll(".katex-mathml");

				// 1. 数式が含まれている PRE
				if (formulas.length > 0) {
					const lines = [];
					formulas.forEach(katex => {
						const anno = katex.querySelector(
							'annotation[encoding="application/x-tex"]'
						);
						if (anno) {
							lines.push(`$$${anno.textContent.trim()}$$`);
						}
					});
					return `\n\`\`\`\n${lines.join("\n")}\n\`\`\`\n\n`;
				}

				// 2. 純テキスト PRE(入力例・出力例)
				const text = node.textContent
					.replace(/\n+$/, "")   // 末尾の改行整理
					.replace(/^\n+/, "");  // 先頭の改行整理

				return `\n\`\`\`\n${text}\n\`\`\`\n\n`;
			}

			// --- 再帰 ---
			let children = "";
			node.childNodes.forEach(child => {
				children += walk(child, inPre);
			});

			// --- Markdown ---
			switch (tag) {
				case "H1":
					return `# ${children.trim()}\n\n`;
				case "H2":
					return `## ${children.trim()}\n\n`;
				case "H3":
					return `### ${children.trim()}\n\n`;
				case "P":
					return `${children.trim()}\n\n`;
				case "LI":
					return `- ${children.trim()}\n`;
				case "BR":
					return "\n";
				case "VAR":
					if (inPre) return "";
					const t = children.trim();
					if (!t) return "";
					return t.startsWith("$") ? t : `$${t}$`;
				default:
					return children;
			}
		}

		return walk(doc.body).trim();
	}

	main();
})();