Codeforces Better!

Codeforces界面汉化、题目翻译,markdown视图,一键复制题目

当前为 2023-05-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Codeforces Better!
// @namespace    https://greasyfork.org/users/747162
// @version      1.08
// @description  Codeforces界面汉化、题目翻译,markdown视图,一键复制题目
// @author       北极小狐
// @match        https://codeforces.com/*
// @connect      www2.deepl.com
// @connect      m.youdao.com
// @connect      openai.api2d.net
// @connect      api.openai.com
// @grant        GM_xmlhttpRequest
// @grant        GM_info
// @connect      greasyfork.org
// @icon         https://aowuucdn.oss-cn-beijing.aliyuncs.com/codeforces.png
// @require      https://cdn.bootcdn.net/ajax/libs/turndown/7.1.1/turndown.min.js
// @license      MIT
// ==/UserScript==
// 样式
function loadCssCode(code){
    var style = document.createElement('style');
    style.type = 'text/css';
    style.rel = 'stylesheet';
    style.appendChild(document.createTextNode(code));
    var head = document.getElementsByTagName('head')[0];
    head.appendChild(style);
}
loadCssCode(`
span.mdViewContent {
    white-space: pre-wrap;
}
.translate-problem-statement {
    white-space: pre-wrap;
    border: 1px dashed #00aeeccc;
    border-radius: 0.3rem;
    padding: 5px;
    margin: 5px 0px;
}
.html2md-panel {
    display: flex;
    justify-content: flex-end;
}
button.html2mdButton {
    height: 3vh;
    width: 3vh;
}
button.html2mdButton {
    cursor: pointer;
    background-color: #e6e6e6;
    color: #727378;
    height: 3vh;
    width: auto;
    font-size: 1.3vh;
    border-radius: 0.3rem;
    border: none;
    padding: 1px 5px;
    margin: 5px;
    box-shadow: 0 0 1px #0000004d;
}
button.html2mdButton.copied {
    background-color: #07e65196;
    color: #104f2b;
}
button.html2mdButton.mdViewed {
    background-color: #ff980057;
    color: #5a3a0c;
}
button.translated {
    cursor: not-allowed;
    background-color: #2a6dc296;
    color: #fffdfd;
}
/*设置面板*/
#CFBetter_setting_menu {
    z-index: 9999;
    border-radius: 0.5rem;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
    display: grid;
    position: fixed;
    top: 50%;
    left: 50%;
    width: 270px;
    transform: translate(-50%, -50%);
    border-radius: 16px;
    background-color: #ecf0ff;
    border: 6px solid #ffffff;
    color: #697e91;
    padding: 10px 20px 20px 20px;
}

#CFBetter_setting_menu .tool-box {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  top: 3px;
  right: 3px;
}

#CFBetter_setting_menu .btn-close {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 2rem;
  height: 2rem;
  color: transparent;
  font-size: 0;
  cursor: pointer;
  background-color: #ff000080;
  border: none;
  border-radius: 10px;
  transition: .2s ease all;
}

#CFBetter_setting_menu .btn-close:hover {
  width: 2rem;
  height: 2rem;
  font-size: 1rem;
  color: #ffffff;
  background-color: #ff0000cc;
  box-shadow: 0 5px 5px 0 #00000026;
}

#CFBetter_setting_menu .btn-close:active {
  width: .9rem;
  height: .9rem;
  font-size: .9rem;
  color: #ffffffde;
  --shadow-btn-close: 0 3px 3px 0 #00000026;
  box-shadow: var(--shadow-btn-close);
}

#CFBetter_setting_menu>label {
    display: flex;
    list-style-type: none;
    padding-inline-start: 0px;
    overflow-x: auto;
    max-width: 100%;
    margin: 0px;
    align-items: center;
    margin: 3px 0px;
}

.CFBetter_setting_menu_label_text {
    border: 1px dashed #00aeeccc;
    display: block;
    height: 20px;
    width: 100%;
    color: gray;
    font-weight: 300;
    font-size: 14px;
    letter-spacing: 2px;
    padding: 7px;
}

input[type="radio"]:checked+.CFBetter_setting_menu_label_text {
    background: #41e49930;
    border: 1px solid green;
    color: green;
    font-weight: 500;
}

#CFBetter_setting_menu>label input[type="radio"] {
    -webkit-appearance: none;
    appearance: none;
    list-style: none;
    margin: 0px;
}

#CFBetter_setting_menu input[type="text"] {
    display: block;
    height: 25px;
    background-color: #ffffff;
    color: #727378;
    font-size: 1.3vh;
    border-radius: 0.3rem;
    padding: 1px 5px;
    margin: 5px 0px 5px 0px;
    border: 1px solid #00aeeccc;
    box-shadow: 0 0 1px #0000004d;
}

.CFBetter_setting_menu_input {
    width: 100%;
    display: grid;
    justify-content: start;
}

#CFBetter_setting_menu #save {
    cursor: pointer;
	display: inline-flex;
	padding: 0.5rem 1rem;
	background-color: #1aa06d;
	color: #ffffff;
	font-size: 1rem;
	line-height: 1.5rem;
	font-weight: 500;
	justify-content: center;
	width: 100%;
	border-radius: 0.375rem;
	border: none;
	box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}

button.html2mdButton.CFBetter_setting {
    float: right;
    background: #60a5fa;
    color: white;
    box-shadow: 0px 0px 1px 1px #0000004d;
    margin: 10px;
}
#CFBetter_setting_menu span.tip {
    color: red;
    font-size: 2px;
    font-weight: 500;
    padding: 0px 0px 10px 0px;
}
/*更新检查*/
div#update_panel {
    position: fixed;
    top: 50%;
    left: 50%;
    width: 240px;
    transform: translate(-50%, -50%);
    background-color: #fdfdfd;
    border: 1px solid #00aeeccc;
    border-radius: 5px;
    box-shadow: 2px 2px 3px 1px #0000004d;
    padding: 10px 20px 20px 20px;
    color: #444242;
    background-color: #ecf0ff;
    border: 6px solid #ffffff;
    border-radius: 16px;
}
div#update_panel #updating {
    cursor: pointer;
	display: inline-flex;
	padding: 0.5rem 1rem;
	background-color: #1aa06d;
	color: #ffffff;
	font-size: 1rem;
	line-height: 1.5rem;
	font-weight: 500;
	justify-content: center;
	width: 100%;
	border-radius: 0.375rem;
	border: none;
	box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
div#update_panel #updating a {
    text-decoration: none;
    color: white;
}
`);

// 更新检查
(function checkScriptVersion() {
    function compareVersions(version1 = "0", version2 = "0") {
        const v1Array = String(version1).split(".");
        const v2Array = String(version2).split(".");
        const minLength = Math.min(v1Array.length, v2Array.length);
        let result = 0;
        for (let i = 0; i < minLength; i++) {
            const curV1 = Number(v1Array[i]);
            const curV2 = Number(v2Array[i]);
            if (curV1 > curV2) {
                result = 1;
                break;
            } else if (curV1 < curV2) {
                result = -1;
                break;
            }
        }
        if (result === 0 && v1Array.length !== v2Array.length) {
            const v1IsBigger = v1Array.length > v2Array.length;
            const maxLenArray = v1IsBigger ? v1Array : v2Array;
            for (let i = minLength; i < maxLenArray.length; i++) {
                const curVersion = Number(maxLenArray[i]);
                if (curVersion > 0) {
                    v1IsBigger ? result = 1 : result = -1;
                    break;
                }
            }
        }
        return result;
    }

    GM_xmlhttpRequest({
        method: "GET",
        url: "https://greasyfork.org/zh-CN/scripts/465777.json",
        timeout: 10 * 1e3,
        onload: function(response) {
            const scriptData = JSON.parse(response.responseText);
            if (scriptData.name === GM_info.script.name && compareVersions(scriptData.version, GM_info.script.version) === 1) {
                $("body").append(`
					<div id='update_panel'>
						<h3>${GM_info.script.name}有新版本!</h3>
						<hr>
						<div class='update_panel_menu'>
                            <span class ='tip'>版本信息:${GM_info.script.version} → ${scriptData.version}</span>
						</div>
						<br>
						<button id='updating'><a target="_blank" href="${scriptData.url}">更新</a></button>
					</div>
				`);
            }
        }
    });
})();

// 汉化替换
(function() {
    // 设置语言为zh
    var htmlTag = document.getElementsByTagName("html")[0];
    htmlTag.setAttribute("lang", "zh-CN");

    // 定义classData,存放元素的class名和对应的替换规则
    const classData = {
        '.menu-list.main-menu-list': [
            { match: 'Help', replace: '帮助' },
            { match: 'Calendar', replace: '日历' },
            { match: 'Edu', replace: '培训' },
            { match: 'Rating', replace: '积分榜' },
            { match: 'Groups', replace: '团体' },
            { match: 'Problemset', replace: '题单' },
            { match: 'Gym', replace: '训练营(过去的大型比赛)' },
            { match: 'Contests', replace: '比赛' },
            { match: 'Catalog', replace: '指南目录' },
            { match: 'Top', replace: '热门' },
            { match: 'Home', replace: '主页' },
        ],
        '.caption.titled': [
            { match: 'Pay attention', replace: '注意' },
            { match: 'Top rated', replace: '积分排行' },
            { match: 'Top contributors', replace: '贡献者排行' },
            { match: 'Find user', replace: '查找用户' },
            { match: 'Recent actions', replace: '最近热帖' },
            { match: 'Training filter', replace: '过滤筛选' },
            { match: 'Find training', replace: '搜索比赛/问题' },
            { match: 'Virtual participation', replace: '什么是虚拟参赛' },
            { match: 'Contest materials', replace: '比赛相关资料' },
            { match: 'Settings', replace: '设置' },
            { match: 'Clone Contest to Mashup', replace: 'Clone比赛到组合混搭' },
            { match: 'Submit', replace: '提交' },
        ],
        '.personal-sidebar ': [
            { match: 'Contribution', replace: '贡献' },
            { match: 'Settings', replace: '设置' },
            { match: 'Blog', replace: '博客' },
            { match: 'Teams', replace: '队伍' },
            { match: 'Submissions', replace: '提交' },
            { match: 'Talks', replace: '私信' },
            { match: 'Contests', replace: '比赛' },
        ],
        '.contest-state-phase': [
            { match: 'Before contest', replace: '即将进行的比赛' },
        ],
        '.act-item': [
            { match: 'Add to favourites', replace: '添加到收藏' },
            { match: 'Submit', replace: '提交' },
        ],
        '.datatable': [
            { match: 'Virtual participation', replace: '参加虚拟重现赛' },
            { match: 'Enter', replace: '进入' },
            { match: 'Final standings', replace: '榜单' },
            { match: 'School/University/City/Region Championship', replace: '学校/大学/城市/区域比赛' },
            { match: 'Official School Contest', replace: '学校官方比赛' },
            { match: 'Training Contest', replace: '训练赛' },
            { match: 'Training Camp Contest', replace: '训练营比赛' },
            { match: 'Official ICPC Contest', replace: 'ICPC官方比赛' },
            { match: 'Official International Personal Contest', replace: '官方国际个人赛' },
            { match: 'China', replace: '中国' },
            { match: 'Statements', replace: '题目描述' },
            { match: 'in Chinese', replace: '中文' },
            { match: 'Trainings', replace: '训练' },
            { match: 'Prepared by', replace: '编写人' },
            { match: 'Current or upcoming contests', replace: '当前或即将举行的比赛' },
            { match: 'Past contests', replace: '过去的比赛' },
            { match: 'Exclusions', replace: '排除' },
            { match: 'Before start', replace: '还有' },
            { match: 'Before registration', replace: '报名还有' },
            { match: 'Until closing ', replace: '结束报名' },
            { match: 'Register', replace: '报名' },
            { match: 'Registration completed', replace: '已报名' },
            { match: 'Questions about problems', replace: '关于题目的提问' },
        ],
        '.ask-question-link': [
            { match: 'Ask a question', replace: '提一个问题' },
        ],
        '.contests-table': [
            { match: 'Contest history', replace: '比赛历史' },
        ],
        '.roundbox.sidebox.borderTopRound ': [
            { match: 'Season:', replace: '时间范围(年度)' },
            { match: 'Contest type', replace: '比赛类型' },
            { match: 'ICPC region', replace: 'ICPC地区' },
            { match: 'Contest format', replace: '比赛形式' },
            { match: 'Duration, hours', replace: '持续时间(小时)' },
            { match: 'Order by', replace: '排序方式' },
            { match: 'Secondary order by', replace: '次要排序方式' },
            { match: 'Hide, if participated', replace: '隐藏我参与过的' },
            { match: 'Hide excluded gyms', replace: '隐藏排除的比赛' },
            { match: 'Register now', replace: '现在报名' },
            { match: 'Show tags for unsolved problems', replace: '显示未解决问题的标签' },
            { match: 'Hide solved problems', replace: '隐藏已解决的问题' },
        ],
        '.icon-eye-close.icon-large': [
            { match: 'Add to exclusions', replace: '添加到排除列表' },
        ],
        '._ContestFilterExclusionsManageFrame_addExclusionLink': [
            { match: 'Add to exclusions for gym contests filter', replace: '添加训练营过滤器的排除项' },
        ],
        '.roundbox.sidebox.sidebar-menu.borderTopRound ': [
            { match: 'Announcement', replace: '公告' },
            { match: 'Statements', replace: '统计报表' },
            { match: 'Tutorial', replace: '题解' },
        ],
        '.second-level-menu ': [
            { match: 'Problems', replace: '问题' },
            { match: 'Submit Code', replace: '提交代码' },
            { match: 'My Submissions', replace: '我的提交' },
            { match: 'Status', replace: '状态' },
            { match: 'Standings', replace: '榜单' },
            { match: 'Custom Invocation', replace: '自定义调试' },
            { match: 'Common standings', replace: '全部排行' },
            { match: 'Friends standings', replace: '只看朋友' },
            { match: 'Submit', replace: '提交' },
            { match: 'Custom test', replace: '自定义测试' },
            { match: 'Blog', replace: '博客' },
            { match: 'Teams', replace: '队伍' },
            { match: 'Submissions', replace: '提交' },
            { match: 'Groups', replace: '团体' },
            { match: 'Contests', replace: '比赛' },
            { match: '问题etting', replace: '参与编写的问题' },
            { match: 'Gym', replace: '训练营' },
            { match: 'Mashups', replace: '组合混搭' },
            { match: 'Posts', replace: '帖子' },
            { match: 'Comments', replace: '回复' },
            { match: 'Main', replace: '主要' },
            { match: 'Settings', replace: '设置' },
            { match: 'Lists', replace: '列表' },
        ],
        '.topic-toggle-collapse': [
            { match: 'Expand', replace: '展开' },
        ],
        '.topic-read-more': [
            { match: 'Full text and comments', replace: '阅读全文/评论' },
        ],
        '.toggleEditorCheckboxLabel': [
            { match: 'Switch off editor', replace: '关闭编辑器语法高亮' },
        ],
        '.content-with-sidebar': [
            { match: 'Notice', replace: '注意' },
            { match: 'virtual participation', replace: '虚拟参与' },
            { match: 'Registration for the contest', replace: '比赛报名' },
            { match: 'Terms of agreement', replace: '协议条款' },
            { match: 'Take part', replace: '参与' },
            { match: 'as individual participant', replace: '作为个人参与者' },
            { match: 'as a team member', replace: '作为团队成员' },
            { match: 'Virtual start time', replace: '虚拟开始时间' },
            { match: 'Complete problemset', replace: '完整的问题集' },
        ],
        '.submit': [
            { match: 'Registration for the contest', replace: '比赛报名' },
        ],
        '.table-form': [
            { match: 'Problem', replace: '题目' },
            { match: 'Language', replace: '语言' },
            { match: 'Source code', replace: '源代码' },
            { match: 'Or choose file', replace: '或者选择文件' },
            { match: 'Choose file', replace: '选择文件' },
        ],
    };

    // 将所有 class 名与之符合的元素的 text 中包含的匹配的文字均替换为对应的文字
    for (const className in classData) {
        const elements = document.querySelectorAll(className);
        elements.forEach((element) => {
            let html = element.outerHTML; // 获取元素的 html 代码
            const parent = element.parentNode;
            const childIndex = Array.from(parent.children).indexOf(element);
            let matched = false; // 标记该元素是否匹配了规则
            classData[className].forEach((rule) => {
                if (html.match(new RegExp(rule.match, 'g'))) {
                    // 如果匹配成功,则将 matched 设置为 true
                    matched = true;
                    html = html.replace(new RegExp(rule.match, 'g'), rule.replace); // 将其中匹配的文字替换为对应的文字
                    const temp = document.createElement('div'); // 创建临时元素
                    temp.innerHTML = html.trim();
                    const newElement = element.cloneNode(true);
                    newElement.innerHTML = temp.firstChild.innerHTML;
                    parent.replaceChild(newElement, parent.children[childIndex]); // 替换元素
                }
            });
        });
    }
})();

// 设置面板
$("div[class='lang-chooser']").each(function() {
    $(this).before(
        "<button class='html2mdButton CFBetter_setting'>CodeforcesBetter设置</button>"
    );
});
$(document).ready(function () {
    $(".CFBetter_setting").click(function () {
        $(".CFBetter_setting").attr("disabled", true);
        $(".CFBetter_setting").css("background-color", "#e6e6e6");
        $(".CFBetter_setting").css("color", "#727378");
        $(".CFBetter_setting").css("cursor", "not-allowed");
        $("body").append(`
					<div id='CFBetter_setting_menu'>
                        <div class="tool-box">
		                    <button class="btn-close">×</button>
	                    </div>
						<h3>翻译设置</h3>
						<hr>
						<label>
							<input type='radio' name='translation' value='deepl'>
							<span class='CFBetter_setting_menu_label_text'>deepl翻译</span>
						</label>
						<label>
							<input type='radio' name='translation' value='youdao'>
							<span class='CFBetter_setting_menu_label_text'>有道翻译</span>
						</label>
						<label>
							<input type='radio' name='translation' value='openai'>
							<span class='CFBetter_setting_menu_label_text'>使用ChatGPT翻译(API)</span>
						</label>
						<label>
							<input type='radio' name='translation' value='api2d'>
							<span class='CFBetter_setting_menu_label_text'>使用api2d翻译(API)</span>
							</label><br>
						<div class='CFBetter_setting_menu_input' id='baidu' style='display: none;'>
							<label for='baidu_uid'>APP ID:</label><input type='text' id='baidu_uid'>
							<label for='baidu_key'>KEY:</label><input type='text' id='baidu_key'>
						</div>
						<div class='CFBetter_setting_menu_input' id='openai' style='display: none;'>
                            <span class ='tip'>提示:<br>使用ChatGPT-3.5,脚本的所有请求均在本地完成,<br>请确保你能够正常访问OpenAI的api</span>
                            <span class ='tip'>你需要输入自己的OpenAI KEY,<a target="_blank" href="https://platform.openai.com/account/usage">官网</a></span>
							<label for='openai_key'>KEY:</label><input type='text' id='openai_key'>
						</div>
						<div class='CFBetter_setting_menu_input' id='api2d' style='display: none;'>
                            <span class ='tip'>提示:<br>api2d是国内的一家提供代理直连访问OpenAI的api的服务商,相当于OpenAI的api的套壳<br></span>
                            <span class ='tip'>你需要输入自己的api2d KEY,<a target="_blank" href="https://api2d.com/profile">官网</a></span>
							<label for='api2d_key'>KEY:</label><input type='text' id='api2d_key'>
						</div>
						<br>
						<button id='save'>保存</button>
					</div>
				`);
        var translation = getCookie("translation");
        if (translation === 'undefined' || translation === '') {
            setCookie("translation", "deepl", 3650);
            $("input[name='translation'][value='deepl']").prop("checked", true);
        } else {
            $("input[name='translation'][value='" + translation + "']").prop("checked", true);
            $("input[name='translation']").css("color", "gray");
            if (translation == "baidu") {
                $("#baidu").show();
                $("#baidu_uid").val(getCookie("baidu_uid"));
                $("#baidu_key").val(getCookie("baidu_key"));
                $("#baidu_uid").css("color", "gray");
                $("#baidu_key").css("color", "gray");
            } else if (translation == "openai") {
                $("#openai").show();
                $("#openai_key").val(getCookie("openai_key"));
                $("#openai_key").css("color", "gray");
            } else if (translation == "api2d") {
                $("#api2d").show();
                $("#api2d_key").val(getCookie("api2d_key"));
                $("#api2d_key").css("color", "gray");
            }
        }

        // 当单选框被选中时,显示对应的输入框,同时隐藏其他输入框
        $("input[name='translation']").change(function () {
            var selected = $(this).val(); // 获取当前选中的值
            if (selected === 'baidu') {
                $("#baidu").show();
                $("#baidu_uid").val(getCookie("baidu_uid"));
                $("#baidu_key").val(getCookie("baidu_key"));
                $("#openai, #api2d").hide();
            } else if (selected === 'openai') {
                $("#openai").show();
                $("#openai_key").val(getCookie("openai_key"));
                $("#baidu, #api2d").hide();
            } else if (selected === 'api2d') {
                $("#api2d").show();
                $("#api2d_key").val(getCookie("api2d_key"));
                $("#baidu, #openai").hide();
            } else {
                $("#baidu, #openai, #api2d").hide();
            }
        });

        $("#save").click(function () {
            var translation = $("input[name='translation']:checked").val();
            var baidu_uid = $("#baidu_uid").val();
            var baidu_key = $("#baidu_key").val();
            var openai_key = $("#openai_key").val();
            var api2d_key = $("#api2d_key").val();
            setCookie("translation", translation, 3650);
            if (translation == "baidu") {
                setCookie("baidu_uid", baidu_uid, 3650);
                setCookie("baidu_key", baidu_key, 3650);
            } else if (translation == "openai") {
                setCookie("openai_key", openai_key, 3650);
            } else if (translation == "api2d") {
                setCookie("api2d_key", api2d_key, 3650);
            }
            location.reload();
        });
        // 关闭
        $("#CFBetter_setting_menu .btn-close").click(function () {
            $("#CFBetter_setting_menu").remove();
            $(".CFBetter_setting").attr("disabled", false);
            $(".CFBetter_setting").css("background-color", "#60a5fa");
            $(".CFBetter_setting").css("color", "white");
            $(".CFBetter_setting").css("cursor", "pointer");
        });
    });
});

// 题目markdown转换/翻译面板
window.onload = function() {
    let turndownService = new TurndownService();

    turndownService.keep(['del']);

    // **处理规则**
    // 忽略sample-tests
    turndownService.addRule('ignore-sample-tests', {
        filter: function(node) {
            return node.classList.contains('sample-tests')|| node.classList.contains('header');
        },
        replacement: function (content, node) {
            return "";
        }
    });

    // remove <script> math
    turndownService.addRule('remove-script', {
        filter: function (node, options) {
            return node.tagName.toLowerCase() == "script" && node.type.startsWith("math/tex");
        },
        replacement: function (content, node) {
            return "";
        }
    });

    // inline math
    turndownService.addRule('inline-math', {
        filter: function (node, options) {
            return node.tagName.toLowerCase() == "span" && node.className == "MathJax";
        },
        replacement: function (content, node) {
            return "$ " + $(node).next().text() + " $";
        }
    });

    // block math
    turndownService.addRule('block-math', {
        filter: function (node, options) {
            return node.tagName.toLowerCase() == "div" && node.className == "MathJax_Display";
        },
        replacement: function (content, node) {
            return "\n$$\n" + $(node).next().text() + "\n$$\n";
        }
    });

    // **题干**
    // 添加按钮
    $("div[class='problem-statement']").each(function() {
        $(this).children(':eq(1)').before(
            "<div class='html2md-panel'> <button class='html2mdButton html2md-view1'>MarkDown视图</button> <button class='html2mdButton html2md-cb1'>Copy</button> <button class='html2mdButton translateButton1'>翻译</button> </div>"
        );
    });


    $(".html2md-cb1").click(function() {
        let target = $(this).parent().next().get(0);
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
        $(this).addClass("copied");
        $(this).text("Copied");
        // 更新复制按钮文本
        setTimeout(() => {
            $(this).removeClass("copied");
            $(this).text("Copy");
        }, 2000);
    });

    $(".html2md-view1").click(function() {
        let target = $(this).parent().next().get(0);
        if (target.viewmd) {
            target.viewmd = false;
            $(this).text("MarkDown视图");
            $(this).removeClass("mdViewed");
            $(target).html(target.original_html);
        } else {
            target.viewmd = true;
            if (!target.original_html)
                target.original_html = $(target).html();
            if (!target.markdown)
                target.markdown = turndownService.turndown($(target).html());
            $(this).text("原始内容");
            $(this).addClass("mdViewed");
            $(target).html(`<span class="mdViewContent" oninput="$(this).parent().get(0).markdown=this.value;" style="width:auto; height:auto;"> ${target.markdown} </span>`);
        }
    });
    $(".translateButton1").click(async function() {
        let target = $(this).parent().next().get(0);
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        // 翻译处理
        $(this).text("翻译中");
        $(this).css("cursor", "not-allowed");
        var element_node = $("div.problem-statement").children(':eq(2)').get(0);
        await translateProblemStatement(textarea.value, element_node);
        //
        $(this).addClass("translated");
        $(this).text("已翻译");
        $(this).prop("disabled",true);
    });
    // **Input**
    // 添加按钮
    $("div[class='input-specification']").each(function() {
        $(this).children(':eq(1)').before(
            "<div class='html2md-panel'> <button class='html2mdButton html2md-view2'>MarkDown视图</button> <button class='html2mdButton html2md-cb2'>Copy</button> <button class='html2mdButton translateButton2'>翻译</button> </div>"
        );
    });


    $(".html2md-cb2").click(function() {
        let RelTarget = $(this).parent().parent().get(0);
        let target = $(RelTarget).clone(); // 创建副本
        $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
        $(target).children(':first').remove();
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
        $(this).addClass("copied");
        $(this).text("Copied");
        // 更新复制按钮文本
        setTimeout(() => {
            $(this).removeClass("copied");
            $(this).text("Copy");
        }, 2000);
        $(target).remove();
    });

    $(".html2md-view2").click(function() {
        let target = $(this).parent().parent().get(0);
        // 临时删除前两个子元素(标题和按钮面板)
        var removedChildren = $(this).parent().parent().children().eq(0).add($(this).parent().parent().children().eq(1)).detach();
        //
        if (target.viewmd) {
            target.viewmd = false;
            $(this).text("MarkDown视图");
            $(this).removeClass("mdViewed");
            $(target).html(target.original_html);
        } else {
            target.viewmd = true;
            if (!target.original_html)
                target.original_html = $(target).html();
            if (!target.markdown)
                target.markdown = turndownService.turndown($(target).html());
            $(this).text("原始内容");
            $(this).addClass("mdViewed");
            $(target).html(`<span class="mdViewContent oninput="$(this).parent().get(0).markdown=this.value;" style="width:auto; height:auto;"> ${target.markdown} </span>`);
        }
        // 恢复删除的元素
        $(".input-specification").prepend(removedChildren);
    });
    $(".translateButton2").click(async function() {
        let RelTarget = $(this).parent().parent().get(0);
        let target = $(RelTarget).clone(); // 创建副本
        $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
        $(target).children(':first').remove();
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        // 翻译处理
        $(this).text("翻译中");
        $(this).css("cursor", "not-allowed");
        var element_node = $("div.input-specification").get(0);
        await translateProblemStatement(textarea.value, element_node);
        //
        $(this).addClass("translated");
        $(this).text("已翻译");
        $(this).prop("disabled",true);
        $(target).remove();
    });
    // **Output**
    // 添加按钮
    $("div[class='output-specification']").each(function() {
        $(this).children(':eq(1)').before(
            "<div class='html2md-panel'> <button class='html2mdButton html2md-view3'>MarkDown视图</button> <button class='html2mdButton html2md-cb3'>Copy</button> <button class='html2mdButton translateButton3'>翻译</button> </div>"
        );
    });


    $(".html2md-cb3").click(function() {
        let RelTarget = $(this).parent().parent().get(0);
        let target = $(RelTarget).clone(); // 创建副本
        $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
        $(target).children(':first').remove();
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
        $(this).addClass("copied");
        $(this).text("Copied");
        // 更新复制按钮文本
        setTimeout(() => {
            $(this).removeClass("copied");
            $(this).text("Copy");
        }, 2000);
        $(target).remove();
    });

    $(".html2md-view3").click(function() {
        let target = $(this).parent().parent().get(0);
        // 临时删除前两个子元素(标题和按钮面板)
        var removedChildren = $(this).parent().parent().children().eq(0).add($(this).parent().parent().children().eq(1)).detach();
        //
        if (target.viewmd) {
            target.viewmd = false;
            $(this).text("MarkDown视图");
            $(this).removeClass("mdViewed");
            $(target).html(target.original_html);
        } else {
            target.viewmd = true;
            if (!target.original_html)
                target.original_html = $(target).html();
            if (!target.markdown)
                target.markdown = turndownService.turndown($(target).html());
            $(this).text("原始内容");
            $(this).addClass("mdViewed");
            $(target).html(`<span class="mdViewContent" oninput="$(this).parent().get(0).markdown=this.value;" style="width:auto; height:auto;"> ${target.markdown} </span>`);
        }
        // 恢复删除的元素
        $(".output-specification").prepend(removedChildren);
    });
    $(".translateButton3").click(async function() {
        let RelTarget = $(this).parent().parent().get(0);
        let target = $(RelTarget).clone(); // 创建副本
        $(target).children(':first').remove(); // 删除前两个元素(标题和按钮面板)
        $(target).children(':first').remove();
        if (!target.markdown){
            target.markdown = turndownService.turndown($(target).html());
        }
        const textarea = document.createElement('textarea');
        textarea.value = target.markdown;
        // 翻译处理
        $(this).text("翻译中");
        $(this).css("cursor", "not-allowed");
        var element_node = $("div.output-specification").get(0);
        await translateProblemStatement(textarea.value, element_node);
        //
        $(this).addClass("translated");
        $(this).text("已翻译");
        $(this).prop("disabled",true);
        $(target).remove();
    });
};

// **翻译框/翻译处理器**
var translatedText = "";
async function translateProblemStatement(text, element_node){
    let id = Math.floor(Date.now() / 1000);
    let matches;
    let replacements;
    // 创建元素并放在element_node的后面
    const translateDiv = document.createElement('div');
    translateDiv.setAttribute('id', id);
    translateDiv.classList.add('translate-problem-statement');
    const spanElement = document.createElement('span');
    translateDiv.appendChild(spanElement);
    element_node.insertAdjacentElement('afterend', translateDiv);
    // 替换latex公式
    if(getCookie("translation")!= "api2d"&&getCookie("translation")!= "openai"){
        matches = text.match(/\$(.*?)\$/g);
        replacements = {};
        try{
            for (let i = 0; i < matches.length; i++) {
                let match = matches[i];
                text = text.replace(match, `【${i + 1}】`);
                replacements[`【${i + 1}】`] = match;
            }
        }catch(e){}
        text = text.replace(/\\/g, "");
    }
    // 翻译
    var translation = getCookie("translation");
    if(translation == "deepl") {
        translateDiv.textContent = "正在翻译中……请耐心等待,\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
        translatedText = await translate_deepl(text);
    }else if (translation == "youdao" ){
        translateDiv.textContent = "正在翻译中……请耐心等待,\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
        translatedText = await translate_youdao_mobile(text);
    }else if (translation == "baidu") {
        var baidu_appid = getCookie("baidu_uid");
        var baidu_key = getCookie("baidu_key");
    } else if (translation == "openai") {
        translateDiv.textContent = "正在翻译中……请稍等\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
        translatedText = await translate_openai(text);
    } else if (translation == "api2d") {
        translateDiv.textContent = "正在翻译中……请稍等\n\n使用GPT(ChatGPT/api2d)进行翻译通常需要很长的时间,请耐心等待\n\n如果长时间无变化,请尝试刷新页面重试或者查看控制台的报错信息";
        translatedText = await translate_api2d(text);
    }
    // 还原latex公式
    if(getCookie("translation")!= "api2d"&&getCookie("translation")!= "openai"){
        try{
            for (let i = 0; i < matches.length; i++) {
                let match = matches[i];
                let replacement = replacements[`【${i + 1}】`];
                translatedText = translatedText.replace(`【${i + 1}】`, replacement);
            }
        }catch(e){}
    }
    // 使符合mathjx的转换语法
    translatedText = translatedText.replace(/\$/g, "$$$$$$");
    //
    // 更新
    translateDiv.textContent = translatedText;
    // 渲染Latex
    MathJax.Hub.Queue(["Typeset", MathJax.Hub, document.getElementById(id)]);
}

// ChatGPT
async function translate_openai(raw) {
    var openai_key = getCookie("openai_key");
    var openai_retext = "";
    const data = {
        prompt: "(You:请帮我翻译将下面的文本翻译为中文,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n"+ raw + ")",
        max_tokens: 2048,
        model: "text-davinci-003",
    }
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'https://api.openai.com/v1/completions',
            data: JSON.stringify(data),
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + getCookie("openai_key") + ''
            },
            responseType: 'json',
            onload: function (res) {
                if (res.status === 200) {
                    openai_retext = res.response.choices[0].text;
                    openai_retext = openai_retext.replace(/^\s+/, '');
                    resolve(openai_retext);
                }
                else {
                    console.error(res.statusText);
                    reject(res.statusText);
                }
            }
        });
    });
}

// api2d
async function translate_api2d(raw) {
    var api2d_key = getCookie("api2d_key");
    var api2d_retext = "";
    const postData = JSON.stringify({
        model: 'gpt-3.5-turbo',
        messages: [{ role: 'user', content: '请帮我翻译将下面的文本翻译为中文,注意保持其中的latex公式不翻译,你只需要回复翻译后的内容即可,不要回复任何其他内容:\n'+ raw }],
    });

    const options = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            Authorization: 'Bearer '+api2d_key,
        },
        data: postData,
    };

    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: options.method,
            url: `https://openai.api2d.net/v1/chat/completions`,
            headers: options.headers,
            data: options.data,
            responseType: 'json',
            onload: function (response) {
                api2d_retext = response.response.choices[0].message.content;
                resolve(api2d_retext);
            },
            onerror: function (response) {
                console.error(response.statusText);
                reject(response.statusText);
            },
        });
    });
}
//

//--有道翻译m--start
async function translate_youdao_mobile(raw){
    const options = {
        method:"POST",
        url:'http://m.youdao.com/translate',
        data:"inputtext="+encodeURIComponent(raw)+"&type=AUTO",
        anonymous:true,
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    }
    return await BaseTranslate('有道翻译mobile',raw,options,res=>/id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res)[1])
}
//--有道翻译m--end

//--Deepl翻译--start
function getTimeStamp(iCount) {
    const ts = Date.now();
    if (iCount !== 0) {
        iCount = iCount + 1;
        return ts - (ts % iCount) + iCount;
    } else {
        return ts;
    }
}

async function translate_deepl(raw) {
    const id = (Math.floor(Math.random() * 99999) + 100000)* 1000;
    const data = {
        jsonrpc: '2.0',
        method: 'LMT_handle_texts',
        id,
        params: {
            splitting: 'newlines',
            lang: {
                source_lang_user_selected: 'auto',
                target_lang: 'ZH',
            },
            texts: [{
                text: raw,
                requestAlternatives:3
            }],
            timestamp: getTimeStamp(raw.split('i').length - 1)
        }
    }
    let postData = JSON.stringify(data);
    if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
        postData = postData.replace('"method":"', '"method" : "');
    } else {
        postData = postData.replace('"method":"', '"method": "');
    }
    const options = {
        method: 'POST',
        url: 'https://www2.deepl.com/jsonrpc',
        data: postData,
        headers: {
            'Content-Type': 'application/json',
            'Host': 'www.deepl.com',
            'Origin': 'https://www.deepl.com',
            'Referer': 'https://www.deepl.com/'
        },
        anonymous:true,
        nocache:true,
    }
    return await BaseTranslate('Deepl翻译',raw,options,res=>JSON.parse(res).result.texts[0].text)
}

//--Deepl翻译--end

// **Cookie Get/Set**
function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
    var expires = "expires=" + d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}


//--异步请求包装工具--start
async function PromiseRetryWrap(task,options,...values){
    const {RetryTimes,ErrProcesser} = options||{};
    let retryTimes = RetryTimes||5;
    const usedErrProcesser = ErrProcesser || (err =>{throw err});
    if(!task)return;
    while(true){
        try{
            return await task(...values);
        }catch(err){
            if(!--retryTimes){
                console.log(err);
                return usedErrProcesser(err);
            }
        }
    }
}

async function BaseTranslate(name,raw,options,processer){
    const toDo = async ()=>{
        var tmp;
        try{
            const data = await Request(options);
            tmp = data.responseText;
            const result = await processer(tmp);
            if(result)sessionStorage.setItem(name+'-'+raw,result);
            return result
        }catch(err){
            throw {
                responseText: tmp,
                err: err
            }
        }
    }
    return await PromiseRetryWrap(toDo,{RetryTimes:3,ErrProcesser:()=>"翻译出错"})
}

function Request(options){
    return new Promise((reslove,reject)=>GM_xmlhttpRequest({...options,onload:reslove,onerror:reject}))
}

//--异步请求包装工具--end