AtCoderProblemCopier

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

Versión del día 10/02/2026. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         AtCoderProblemCopier
// @name:en      AtCoderProblemCopier
// @name:zh-CN   AtCoderProblemCopier
// @name:zh-TW   AtCoderProblemCopier
// @namespace    https://github.com/HHayasaka25/AtCoder-Problem-Copier
// @version      1.0.0
// @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();
})();