Greasy Fork is available in English.

AtCoderProblemCopier

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

La data de 10-02-2026. Vezi ultima versiune.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();