Codeforces Better!

一个适用于 Codeforces 的 Tampermonkey 脚本,增强功能与界面。

当前为 2024-06-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Codeforces Better!
  3. // @namespace https://greasyfork.org/users/747162
  4. // @version 1.75.0
  5. // @author 北极小狐
  6. // @match *://*.codeforces.com/*
  7. // @match *://*.codeforc.es/*
  8. // @run-at document-start
  9. // @connect www2.deepl.com
  10. // @connect api-free.deepl.com
  11. // @connect api.deepl.com
  12. // @connect api.deeplx.org
  13. // @connect www.iflyrec.com
  14. // @connect dict.youdao.com
  15. // @connect api.interpreter.caiyunai.com
  16. // @connect translate.google.com
  17. // @connect openai.api2d.net
  18. // @connect api.openai.com
  19. // @connect www.luogu.com.cn
  20. // @connect vjudge.net
  21. // @connect clist.by
  22. // @connect greasyfork.org
  23. // @connect rextester.com
  24. // @connect wandbox.org
  25. // @connect staticfile.net
  26. // @connect aowuucdn.oss-cn-beijing.aliyuncs.com
  27. // @connect aowuucdn.oss-accelerate.aliyuncs.com
  28. // @connect 127.0.0.1
  29. // @connect *
  30. // @grant GM_xmlhttpRequest
  31. // @grant GM_info
  32. // @grant GM_setValue
  33. // @grant GM_getValue
  34. // @grant GM_listValues
  35. // @grant GM_deleteValue
  36. // @grant GM_addStyle
  37. // @grant GM_setClipboard
  38. // @grant GM_getResourceText
  39. // @icon https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png
  40. // @require https://cdn.staticfile.net/turndown/7.1.2/turndown.min.js
  41. // @require https://cdn.staticfile.net/markdown-it/13.0.1/markdown-it.min.js
  42. // @require https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
  43. // @require https://cdn.staticfile.net/chroma-js/2.4.2/chroma.min.js
  44. // @require https://cdn.staticfile.net/xterm/3.9.2/xterm.min.js
  45. // @require https://cdn.staticfile.net/dexie/3.2.4/dexie.min.js
  46. // @require https://cdn.staticfile.net/i18next/23.5.1/i18next.min.js
  47. // @require https://cdn.staticfile.net/i18next-http-backend/2.2.2/i18nextHttpBackend.min.js
  48. // @require https://cdn.staticfile.net/jquery-i18next/1.2.1/jquery-i18next.min.js
  49. // @require https://cdn.staticfile.net/highlight.js/11.3.1/highlight.min.js
  50. // @require https://cdn.staticfile.net/dialog-polyfill/0.5.6/dialog-polyfill.min.js
  51. // @require https://update.greasyfork.org/scripts/484742/1311040/i18nextChainedBackendjs.js
  52. // @require https://update.greasyfork.org/scripts/484743/1311041/i18next-localstorage-backendjs.js
  53. // @resource acwing_cpp_code_completer https://aowuucdn.oss-accelerate.aliyuncs.com/acwing_cpp_code_completer-0.0.11.json
  54. // @resource wandboxlist https://wandbox.org/api/list.json
  55. // @resource xtermcss https://cdn.staticfile.net/xterm/3.9.2/xterm.min.css
  56. // @resource selectpagecss https://aowuucdn.oss-accelerate.aliyuncs.com/css/selectpage.css
  57. // @resource dialogpolyfillcss https://cdn.staticfile.net/dialog-polyfill/0.5.6/dialog-polyfill.min.css
  58. // @license GPL3
  59. // @compatible Chrome
  60. // @compatible Firefox
  61. // @compatible Edge
  62. // @incompatible safari
  63. // @supportURL https://github.com/beijixiaohu/OJBetter/issues
  64. // @name:zh-TW Codeforces Better!
  65. // @name:en Codeforces Better!
  66. // @name:de Codeforces Better!
  67. // @name:fr Codeforces Better!
  68. // @name:ko Codeforces Better!
  69. // @name:pt Codeforces Better!
  70. // @name:ja Codeforces Better!
  71. // @name:es Codeforces Better!
  72. // @name:it Codeforces Better!
  73. // @name:hi Codeforces Better!
  74. // @description 一个适用于 Codeforces 的 Tampermonkey 脚本,增强功能与界面。
  75. // @description:zh-TW 一個適用於 Codeforces 的 Tampermonkey 腳本,增強功能與界面。
  76. // @description:en A Tampermonkey script for Codeforces that enhances functionality and interface.
  77. // @description:de Ein Tampermonkey-Skript für Codeforces, das Funktionalität und Benutzeroberfläche verbessert.
  78. // @description:fr Un script Tampermonkey pour Codeforces qui améliore les fonctionnalités et l'interface.
  79. // @description:ko Codeforces를 위한 Tampermonkey 스크립트로 기능과 인터페이스를 개선합니다.
  80. // @description:pt Um script Tampermonkey para Codeforces que aprimora a funcionalidade e a interface.
  81. // @description:ja Codeforces用のTampermonkeyスクリプトで機能とインターフェースを強化します。
  82. // @description:es Un script Tampermonkey para Codeforces que mejora la funcionalidad y la interfaz.
  83. // @description:it Uno script Tampermonkey per Codeforces che migliora la funzionalità e l'interfaccia.
  84. // @description:hi Codeforces के लिए एक Tampermonkey स्क्रिप्ट जो कार्यक्षमता और इंटरफ़ेस को बेहतर बनाता है।
  85. // ==/UserScript==
  86.  
  87. /**
  88. * @namespace OJBetter
  89. * @desc 主命名空间
  90. */
  91. const OJBetter = {};
  92.  
  93. /**
  94. * @namespace state
  95. * @desc 描述脚本的当前状态。
  96. * @memberof OJBetter
  97. */
  98. OJBetter.state = {
  99. /** @type {string} 脚本名*/
  100. name: GM_info.script.name,
  101. /** @type {string} 格式化后的脚本名*/
  102. formatName: undefined,
  103. /** @type {string} 版本号*/
  104. version: GM_info.script.version,
  105. /** @type {boolean?} 是否跳过页面加载等待 */
  106. notWaiteLoaded: undefined,
  107. /** @type {string} 最后公告版本,用于标识版本更新完成提示 */
  108. lastAnnounceVer: undefined,
  109. /** @type {string} 最后读取的有效公告版本 */
  110. lastReadAnnounceVer: undefined,
  111. /** @type {number} 当前已打开的模态对话框数量*/
  112. openDialogCount: 0
  113. };
  114.  
  115. /**
  116. * @namespace common
  117. * @desc 通用设置和属性。
  118. * @memberof OJBetter
  119. */
  120. OJBetter.common = {
  121. /** @type {string} 网站的主机地址 */
  122. hostAddress: location.origin,
  123. /** @type {string?} Codeforces的CSRF令牌 */
  124. cf_csrf_token: undefined,
  125. /** @type {Array?} 任务队列 */
  126. taskQueue: undefined,
  127. /** @type {object} OJBetter数据库连接实例*/
  128. database: undefined,
  129. /** @type {object} turndownService实例*/
  130. turndownService: undefined,
  131. };
  132.  
  133. /**
  134. * @namespace basic
  135. * @desc 基本的用户界面设置。
  136. * @memberof OJBetter
  137. */
  138. OJBetter.basic = {
  139. /** @type {string} 黑暗模式设置 */
  140. darkMode: undefined,
  141. /** @type {boolean?} 是否展开折叠块 */
  142. expandFoldingblocks: undefined,
  143. /** @type {boolean?} 是否开启折叠块渲染性能优化 */
  144. renderPerfOpt: undefined,
  145. /** @type {boolean?} 是否开启下拉选择框性能优化 */
  146. selectElementPerfOpt: undefined,
  147. /** @type {boolean?} 评论区分页 */
  148. commentPaging: undefined,
  149. /** @type {boolean?} 显示跳转到Luogu按钮 */
  150. showJumpToLuogu: undefined,
  151. /** @type {boolean?} 显示跳转到Virtual Judge按钮 */
  152. showCF2vjudge: undefined,
  153. /** @type {boolean?} 比赛排行榜重新着色 */
  154. standingsRecolor: undefined,
  155. /** @type {boolean?} 隐藏题目问题标签 */
  156. hiddenProblemTag: undefined
  157. };
  158.  
  159. /**
  160. * @namespace typeOfPage
  161. * @desc 页面类型判断。
  162. * @memberof OJBetter
  163. */
  164. OJBetter.typeOfPage = {
  165. /** @type {boolean?} 是否是轻量站 */
  166. is_mSite: undefined,
  167. /** @type {boolean?} 是否是训练营页面 */
  168. is_gym: undefined,
  169. /** @type {boolean?} 是否是acmsguru页面 */
  170. is_acmsguru: undefined,
  171. /** @type {boolean?} 是否是旧版LaTeX页面 */
  172. is_oldLatex: undefined,
  173. /** @type {boolean?} 是否是题目集页面 */
  174. is_contest: undefined,
  175. /** @type {boolean?} 是否是题目页面 */
  176. is_problem: undefined,
  177. /** @type {boolean?} 是否是完整的问题集页面 */
  178. is_completeProblemset: undefined,
  179. /** @type {boolean?} 是否是问题集中的问题页面 */
  180. is_problemset_problem: undefined,
  181. /** @type {boolean?} 是否是问题集页面 */
  182. is_problemset: undefined,
  183. /** @type {boolean?} 是否是Codeforces排名页面 */
  184. is_cfStandings: undefined,
  185. /** @type {boolean?} 是否是提交页面 */
  186. is_submitPage: undefined,
  187. /** @type {boolean?} 是否是代码状态页面 */
  188. is_statePage: undefined,
  189. /** @type {boolean?} 是否是提交记录页面 */
  190. is_submissions: undefined,
  191. /** @type {boolean?} 是否是提交记录详情页面 */
  192. is_submission: undefined,
  193. };
  194.  
  195. /**
  196. * @namespace localization
  197. * @desc 本地化设置。
  198. * @memberof OJBetter
  199. */
  200. OJBetter.localization = {
  201. /** @type {string?} 网站语言 */
  202. websiteLang: undefined,
  203. /** @type {string?} 脚本语言 */
  204. scriptLang: undefined
  205. };
  206.  
  207. /**
  208. * @namespace translation
  209. * @desc 翻译设置。
  210. * @memberof OJBetter
  211. */
  212. OJBetter.translation = {
  213. /** @type {string?} 翻译服务选择 */
  214. choice: undefined,
  215. /** @type {string?} 目标语言 */
  216. targetLang: undefined,
  217. comment: {
  218. /** @type {string?} 评论翻译服务选择 */
  219. choice: undefined,
  220. /** @type {string?} 评论翻译模式 */
  221. transMode: undefined
  222. },
  223. auto: {
  224. /** @type {boolean?} 自动翻译开关 */
  225. enabled: undefined,
  226. /** @type {number?} 短文本长度限制 */
  227. shortTextLength: undefined,
  228. mixTrans: {
  229. /** @type {boolean?} 混合翻译开关 */
  230. enabled: undefined,
  231. /** @type {Array?} 混合翻译服务列表 */
  232. servers: undefined
  233. }
  234. },
  235. memory: {
  236. /** @type {boolean?} 翻译记忆开关 */
  237. enabled: undefined,
  238. /** @type {Object?} 翻译记忆树 */
  239. ttTree: undefined
  240. },
  241. /** @type {string?} 重翻译时的行为 */
  242. retransAction: undefined,
  243. /** @type {number?} 等待时间 */
  244. waitTime: undefined,
  245. /** @type {boolean?} 替换符 */
  246. replaceSymbol: undefined,
  247. /** @type {boolean?} 过滤文本中的*号 */
  248. filterTextWithoutEmphasis: undefined,
  249. /** @type {boolean?} 强制使用turndown转换 */
  250. forceTurndownConversion: undefined
  251. };
  252.  
  253. /**
  254. * @namespace clist
  255. * @desc Clist相关设置。
  256. * @memberof OJBetter
  257. */
  258. OJBetter.clist = {
  259. enabled: {
  260. /** @type {boolean?} 比赛页面开关 */
  261. contest: undefined,
  262. /** @type {boolean?} 问题页面开关 */
  263. problem: undefined,
  264. /** @type {boolean?} 问题集页面开关 */
  265. problemset: undefined
  266. },
  267. /** @type {boolean?} Rating数据防剧透 */
  268. ratingHidden: undefined,
  269. /** @type {string?} Clist key */
  270. authorization: undefined
  271. };
  272.  
  273. /**
  274. * @namespace monaco
  275. * @desc Monaco编辑器配置。
  276. * @memberof OJBetter
  277. */
  278. OJBetter.monaco = {
  279. /** @type {boolean?} 在问题页面上启用Monaco编辑器 */
  280. enableOnProblemPage: undefined,
  281. /** @type {boolean?} 美化pre代码块 */
  282. beautifyPreBlocks: undefined,
  283. /** @type {boolean} Monaco编辑器加载完成标志 */
  284. loaderOnload: false,
  285. lsp: {
  286. /** @type {Array?} LSP套接字数组 */
  287. socket: [],
  288. /** @type {boolean?} 是否启用LSP */
  289. enabled: undefined,
  290. /** @type {string?} 工作路径 */
  291. workUri: undefined,
  292. /** @type {string?} 套接字URL */
  293. socketUrl: undefined
  294. },
  295. complet: {
  296. /** @type {boolean?} 是否启用C++代码补全模板 */
  297. cppCodeTemplate: undefined,
  298. /** @type {Object?} 自定义配置 */
  299. customConfig: undefined
  300. },
  301. /** @type {Object?} Monaco编辑器实例 */
  302. editor: null,
  303. /** @type {string?} 在线编译器选择 */
  304. onlineCompilerChoice: undefined,
  305. /** @type {string?} 记忆编译器语言选择 */
  306. compilerSelection: undefined,
  307. /** @type {string?} 当前选择的语言 */
  308. nowLangSelect: undefined,
  309. setting: {
  310. /** @type {Array?} 语言设置数组 */
  311. language: [],
  312. /** @type {string?} 位置 */
  313. position: undefined,
  314. /** @type {boolean} 位置初始化标志 */
  315. position_initialized: false,
  316. /** @type {number?} 字体大小 */
  317. fontsize: undefined,
  318. /** @type {boolean?} 鼠标滚动锁定 */
  319. alwaysConsumeMouseWheel: undefined,
  320. /** @type {boolean?} 提交代码二次确认 */
  321. isCodeSubmitDoubleConfirm: undefined,
  322. /** @type {boolean?} 测试通过后自动提交 */
  323. autoSubmitAfterPass: undefined,
  324. /** @type {string?} 提交按钮位置 */
  325. submitButtonPosition: undefined,
  326. /** @type {boolean?} 自动保存代码 */
  327. autoMemoryCode: undefined
  328. }
  329. };
  330.  
  331. /**
  332. * @namespace deepl
  333. * @desc DeepL翻译服务配置。
  334. * @memberof OJBetter
  335. */
  336. OJBetter.deepl = {
  337. /** @type {Object?} DeepL配置对象 */
  338. configs: undefined,
  339. config: {
  340. /** @type {string?} 类型 */
  341. type: undefined,
  342. /** @type {string?} 名称 */
  343. name: undefined,
  344. /** @type {string?} API类型 */
  345. apiGenre: undefined,
  346. /** @type {string?} API密钥 */
  347. key: undefined,
  348. /** @type {string?} 代理 */
  349. proxy: undefined,
  350. /** @type {Object?} 额外请求头 */
  351. header: undefined,
  352. /** @type {Object?} 额外请求数据 */
  353. data: undefined,
  354. quota: {
  355. /** @type {string?} 余额URL */
  356. url: undefined,
  357. /** @type {string?} 余额请求方法 */
  358. method: undefined,
  359. /** @type {Object?} 余额请求头 */
  360. header: undefined,
  361. /** @type {Object?} 余额请求数据 */
  362. data: undefined,
  363. /** @type {number?} 剩余配额 */
  364. surplus: undefined
  365. }
  366. },
  367. /** @type {boolean?} 启用重点保护 */
  368. enableEmphasisProtection: undefined,
  369. /** @type {boolean?} 启用链接保护 */
  370. enableLinkProtection: undefined
  371. };
  372.  
  373. /**
  374. * @namespace chatgpt
  375. * @desc ChatGPT服务配置。
  376. * @memberof OJBetter
  377. */
  378. OJBetter.chatgpt = {
  379. /** @type {Object?} ChatGPT配置对象 */
  380. configs: undefined,
  381. config: {
  382. /** @type {string?} 名称 */
  383. name: undefined,
  384. /** @type {string?} 模型 */
  385. model: undefined,
  386. /** @type {string?} API密钥 */
  387. key: undefined,
  388. /** @type {string?} 代理 */
  389. proxy: undefined,
  390. /** @type {Object?} 额外请求头 */
  391. header: undefined,
  392. /** @type {Object?} 额外请求数据 */
  393. data: undefined,
  394. quota: {
  395. /** @type {string?} 余额URL */
  396. url: undefined,
  397. /** @type {string?} 余额请求方法 */
  398. method: undefined,
  399. /** @type {Object?} 余额请求头 */
  400. header: undefined,
  401. /** @type {Object?} 余额请求数据 */
  402. data: undefined,
  403. /** @type {number?} 剩余配额 */
  404. surplus: undefined
  405. }
  406. },
  407. /** @type {boolean?} 是否为流式传输 */
  408. isStream: undefined,
  409. /** @type {string?} 是否使用自定义Prompt */
  410. customPrompt: undefined,
  411. /** @type {boolean?} 是否作为系统Prompt */
  412. asSystemPrompt: undefined
  413. };
  414.  
  415. /**
  416. * @namespace preference
  417. * @desc 偏好设置
  418. * @memberof OJBetter
  419. */
  420. OJBetter.preference = {
  421. /** @type {boolean?} 是否显示加载动画 */
  422. showLoading: undefined,
  423. /** @type {boolean?} 是否显示悬停目标区域 */
  424. hoverTargetAreaDisplay: undefined,
  425. /** @type {string?} 按钮图标大小 */
  426. iconButtonSize: undefined,
  427. };
  428.  
  429. /**
  430. * @namespace dev
  431. * @desc 维护
  432. * @memberof OJBetter
  433. */
  434. OJBetter.dev = {
  435. /** @type {boolean?} 是否显示规则标记 */
  436. isRuleMarkingEnabled: undefined,
  437. };
  438.  
  439. /**
  440. * @namespace about
  441. * @desc 关于页信息
  442. * @memberof OJBetter
  443. */
  444. OJBetter.about = {
  445. /** @type {string?} 更新通道 */
  446. updateChannel: undefined,
  447. /** @type {string?} 更新源 */
  448. updateSource: undefined
  449. };
  450.  
  451. /**
  452. * @namespace supportList
  453. * @desc 支持列表
  454. * @memberof OJBetter
  455. */
  456. OJBetter.supportList = {
  457. /** @type {object} 翻译支持列表和对应语言代码*/
  458. translationSupport: {
  459. 'deepl': { 'zh': 'ZH', 'de': 'DE', 'fr': 'FR', 'ko': 'KO', 'pt': 'PT', 'ja': 'JA', 'es': 'ES', 'it': 'IT' },
  460. 'iflyrec': { 'zh': '1' },
  461. 'youdao': { 'zh': 'zh-CHS', 'zh-Hant': 'zh-CHT', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
  462. 'google': { 'zh': 'zh-CN', 'zh-Hant': 'zh-TW', 'de': 'de', 'fr': 'fr', 'ko': 'ko', 'pt': 'pt', 'ja': 'ja', 'es': 'es', 'it': 'it', 'hi': 'hi' },
  463. 'caiyun': { 'zh': 'auto2zh', 'ja': 'auto2ja', 'ko': 'auto2ko', 'es': 'auto2es', 'fr': 'auto2fr' },
  464. 'openai': { 'zh': 'Chinese', 'zh-Hant': 'Traditional Chinese', 'de': 'German', 'fr': 'French', 'ko': 'Korean', 'pt': 'Portuguese', 'ja': 'Japanese', 'es': 'Spanish', 'it': 'Italian', 'hi': 'Hindi' }
  465. },
  466. /** @type {object} 更新源支持列表*/
  467. updateSourceSupportList: {
  468. 'greasyfork': {
  469. 'release': true,
  470. 'dev': false
  471. },
  472. 'github': {
  473. 'release': true,
  474. 'dev': true
  475. },
  476. 'aliyunoss': {
  477. 'release': true,
  478. 'dev': true
  479. }
  480. }
  481. }
  482.  
  483. // ------------------------------
  484. // 一些工具函数
  485. // ------------------------------
  486.  
  487. /**
  488. * 延迟函数
  489. * @param {number} ms 延迟时间(毫秒)
  490. * @returns {Promise<void>}
  491. */
  492. function OJB_delay(ms) {
  493. return new Promise(resolve => setTimeout(resolve, ms));
  494. }
  495.  
  496. /**
  497. * 等待直到指定的条件函数返回true。
  498. *
  499. * @param {() => boolean} conditionCheck 一个无参数的函数,用于检查条件是否满足。当函数返回true时,表示条件已满足。
  500. * @param {number} [interval=100] 检查条件的间隔时间,单位为毫秒。默认为100毫秒。
  501. * @returns {Promise<void>} 返回一个Promise,在条件满足时解决。
  502. */
  503. async function OJB_waitUntilTrue(conditionCheck, interval = 100) {
  504. return new Promise((resolve) => {
  505. const checkCondition = async () => {
  506. if (conditionCheck()) {
  507. resolve();
  508. } else {
  509. await OJB_delay(interval);
  510. checkCondition();
  511. }
  512. };
  513. checkCondition();
  514. });
  515. }
  516.  
  517. /**
  518. * 动态加载JavaScript库并返回一个Promise,该Promise在脚本加载完成后解决。
  519. *
  520. * @param {string} url - 要加载的JavaScript库的URL地址。
  521. * @returns {Promise<void>} 一个Promise,它在脚本加载并执行完成后解决。
  522. */
  523. function OJB_LoadJS(url) {
  524. return new Promise((resolve, reject) => {
  525. let scriptElement = document.createElement("script");
  526. scriptElement.src = url;
  527. document.head.prepend(scriptElement);
  528. scriptElement.onload = resolve;
  529. scriptElement.onerror = reject;
  530. });
  531. }
  532.  
  533. /**
  534. * 安全地创建JQuery对象
  535. * @description 通过字符串创建JQuery对象时,如果字符串以空格开头,在某些Jquery版本中会发生错误,过滤空格以安全的创建元素。
  536. * @param {string} string - 字符串。
  537. * @returns {JQuery} JQuery对象
  538. */
  539. const OJB_safeCreateJQElement = function (string) {
  540. return $(string.replace(/^\s+/, ""));
  541. }
  542.  
  543. /**
  544. * 将数字或者字符串解析为数字。
  545. * @memberof OJBetter.common
  546. * @param {string} val 要解析的字符串
  547. * @param {boolean} [strict=false] 是否进行严格类型检查
  548. * @returns {number} 解析结果
  549. * @throws {Error} 如果解析失败,则抛出错误
  550. */
  551. const OJB_parseNumber = (val, strict = false) => {
  552. const num = Number(val);
  553. if (isNaN(num) || (strict && val.toString() !== num.toString())) {
  554. throw new Error('Invalid number');
  555. }
  556. return num;
  557. };
  558.  
  559. /**
  560. * 将字符串解析为布尔值
  561. * @param {string} val - 要解析的字符串
  562. * @param {boolean} strict - 是否进行严格类型检查
  563. * @returns {boolean} - 解析结果
  564. * @throws {Error} - 如果解析失败,则抛出错误
  565. */
  566. const OJB_parseBoolean = (val, strict) => {
  567. if (strict) {
  568. if (val === true || val === false) return val;
  569. throw new Error('Invalid boolean');
  570. }
  571. return val === 'true' ? true : val === 'false' ? false : val;
  572. };
  573.  
  574. /**
  575. * 将字符串解析为对象
  576. * @param {string} val - 要解析的字符串
  577. * @returns {Object} - 解析结果
  578. * @throws {Error} - 如果解析失败,则抛出错误
  579. */
  580. const OJB_parseObject = val => {
  581. try {
  582. return JSON.parse(val);
  583. } catch {
  584. throw new Error('Invalid JSON');
  585. }
  586. };
  587.  
  588. /**
  589. * 将字符串解析为键值对数组
  590. * @param {string} val - 要解析的字符串
  591. * @returns {Object[]} - 解析结果
  592. * @throws {Error} - 如果解析失败,则抛出错误
  593. */
  594. const OJB_parseLinePairArray = val => {
  595. if (typeof val !== 'string' || val.trim() === '') return [];
  596. return val.split("\n").filter(line => line.trim() !== '').map(line => {
  597. const indexOfFirstColon = line.indexOf(":");
  598. if (indexOfFirstColon === -1) throw new Error('Invalid LinePairArray format: ":" is missing');
  599. const key = line.substring(0, indexOfFirstColon).trim();
  600. const value = line.substring(indexOfFirstColon + 1).trim();
  601. return { [key]: value };
  602. });
  603. };
  604.  
  605. /**
  606. * 移除文本中的HTML标签
  607. * @param {string} text - 包含HTML标签的文本
  608. * @returns {string} - 移除HTML标签后的文本
  609. */
  610. const OJB_removeHTMLTags = function (text) {
  611. return text.replace(/<\/?[a-zA-Z]+("[^"]*"|'[^']*'|[^'">])*>/g, '');
  612. }
  613.  
  614. /**
  615. * 获取对象中指定路径表达式的值
  616. * @param {Object} obj - 要计算的对象
  617. * @param {string} pathOrExpression - 要计算的路径表达式
  618. * @returns {any} - 计算结果
  619. * @example
  620. * const obj = {
  621. * "a": {
  622. * "b": 1
  623. * },
  624. * "c": 2
  625. * };
  626. * OJB_evaluatePathOrExpression(obj, "a.b"); // 1
  627. * OJB_evaluatePathOrExpression(obj, "a.b + c"); // 3
  628. * OJB_evaluatePathOrExpression(obj, "a.b + a.c"); // 1
  629. */
  630. function OJB_evaluatePathOrExpression(obj, pathOrExpression) {
  631. const hasOperator = /[\+\-\*\/]/.test(pathOrExpression);
  632. const getPathValue = (obj, path) => {
  633. return path.split('.').reduce((acc, part) => {
  634. return acc !== undefined && acc !== null && acc.hasOwnProperty(part) ? acc[part] : undefined;
  635. }, obj);
  636. };
  637. const evaluateExpression = (obj, expression) => {
  638. const tokens = expression.split(/([\+\-\*\/])/).map(token => token.trim());
  639. const values = tokens.map(token => {
  640. if (/[\+\-\*\/]/.test(token)) {
  641. return token;
  642. } else {
  643. const value = getPathValue(obj, token);
  644. return value !== undefined ? value : 0;
  645. }
  646. });
  647. const evaluatedExpression = values.join(' ');
  648. try {
  649. return Function(`'use strict'; return (${evaluatedExpression});`)();
  650. } catch (e) {
  651. console.error('Expression evaluation error:', e);
  652. return undefined;
  653. }
  654. };
  655. return hasOperator ? evaluateExpression(obj, pathOrExpression) : getPathValue(obj, pathOrExpression);
  656. }
  657.  
  658. /**
  659. * 获取 GM 存储的值并根据类型进行处理
  660. * @param {string} key - 要检索的值的键。
  661. * @param {any} defaultValue - 如果值未找到,则返回的默认值。
  662. * @param {Object} [options={}] - 配置选项对象。
  663. * @param {string} [options.type='string'] - 期望的值的类型。可选值:'string', 'number', 'boolean', 'object', 'array', 'linePairArray'。
  664. * @param {boolean} [options.strict=false] - 用于数字和布尔类型,表示是否进行严格类型检查。
  665. * @param {string} [options.pathOrExpression=''] - 用于对象或数组类型,表示路径表达式或获取元素的索引。
  666. * @returns {any} - 检索到的值。
  667. */
  668. const OJB_getGMValue = (key, defaultValue, { type = 'string', strict = false, pathOrExpression = '' } = {}) => {
  669. let value = GM_getValue(key);
  670. if (value === undefined || value === null || value === "") {
  671. GM_setValue?.(key, defaultValue);
  672. return defaultValue;
  673. }
  674.  
  675. const parsers = {
  676. string: val => val,
  677. number: (val) => OJB_parseNumber(val, strict),
  678. boolean: (val) => OJB_parseBoolean(val, strict),
  679. object: OJB_parseObject,
  680. array: OJB_parseObject,
  681. linePairArray: OJB_parseLinePairArray
  682. };
  683.  
  684. if (!(type in parsers)) {
  685. console.error(`Unsupported type: ${type}`);
  686. return defaultValue;
  687. }
  688.  
  689. try {
  690. value = parsers[type](value);
  691. } catch (e) {
  692. console.error('Error:', e.message);
  693. return defaultValue;
  694. }
  695.  
  696. // The pathOrExpression processing is not applicable to linePairArray type
  697. if ((type === 'object' || type === 'array') && pathOrExpression) {
  698. const evaluated = OJB_evaluatePathOrExpression(value, pathOrExpression);
  699. if (evaluated === undefined) {
  700. console.error('Path or expression evaluation returned undefined');
  701. return defaultValue;
  702. }
  703. value = evaluated;
  704. }
  705.  
  706. return value;
  707. };
  708.  
  709. /**
  710. * 版本号比较方法
  711. * @param {string} version1 版本号1
  712. * @param {string} version2 版本号2
  713. * @returns {number} -1: version1 < version2, 0: version1 = version2, 1: version1 > version2
  714. */
  715. const OJB_compareVersions = function (version1 = "0", version2 = "0") {
  716. const v1Array = version1.split(".").map(Number);
  717. const v2Array = version2.split(".").map(Number);
  718. const length = Math.max(v1Array.length, v2Array.length);
  719. for (let i = 0; i < length; i++) {
  720. const diff = (v1Array[i] || 0) - (v2Array[i] || 0);
  721. if (diff) return Math.sign(diff);
  722. }
  723. return 0;
  724. }
  725.  
  726. /**
  727. * 获取上一个主版本号
  728. * @param {string} currentVersion 当前版本号
  729. * @returns {string} 上一个主版本号
  730. */
  731. const OJB_getPreviousVersion = function (currentVersion) {
  732. const versionArray = currentVersion.split(".").map(Number);
  733. let lastNonZeroIndex = versionArray.length - 1;
  734. while (lastNonZeroIndex >= 0 && versionArray[lastNonZeroIndex] === 0) {
  735. lastNonZeroIndex--;
  736. }
  737. if (lastNonZeroIndex >= 0) {
  738. versionArray[lastNonZeroIndex]--;
  739. for (let i = lastNonZeroIndex + 1; i < versionArray.length; i++) {
  740. versionArray[i] = 0;
  741. }
  742. }
  743. return versionArray.join(".");
  744. };
  745.  
  746. /**
  747. * 在指定根节点下观察指定选择器的元素,当元素存在时,执行回调函数
  748. * @param {Object} options - 配置对象
  749. * @param {string} options.selector - CSS选择器文本
  750. * @param {Function} options.callback - 回调函数,接收变动的节点作为参数
  751. * @param {Boolean} [options.triggerOnExist=true] - 如果为true,元素已存在时立即触发一次回调
  752. * @param {Element} [options.root=document.body] - 在哪个根节点下监听变化
  753. * @param {Boolean} [options.subtree=false] - 是否监听子树变化(即非直接子元素)
  754. */
  755. function OJB_observeElement({
  756. selector,
  757. callback,
  758. triggerOnExist = true,
  759. root = document.body,
  760. subtree = false
  761. }) {
  762. // 尝试获取选择器指定的元素
  763. const targetNode = root.querySelector(selector);
  764.  
  765. if (targetNode) {
  766. // 如果元素已存在,直接开始观察
  767. observeAndReport(targetNode, callback);
  768. // 如果triggerOnExist为true,则立即触发一次回调
  769. if (triggerOnExist) {
  770. callback(targetNode);
  771. }
  772. } else {
  773. // 如果元素不存在,监听DOM变化直到该元素被添加
  774. const observer = new MutationObserver((mutations) => {
  775. mutations.forEach((mutation) => {
  776. mutation.addedNodes.forEach((node) => {
  777. if (node.nodeType === Node.ELEMENT_NODE && node.matches(selector)) {
  778. observeAndReport(node, callback);
  779. if (triggerOnExist) {
  780. callback(node);
  781. }
  782. observer.disconnect(); // 停止监听
  783. }
  784. });
  785. });
  786. });
  787.  
  788. observer.observe(root, { childList: true, subtree, attributes: false });
  789. }
  790.  
  791. function observeAndReport(node, callback) {
  792. const childObserver = new MutationObserver((mutations) => {
  793. mutations.forEach((mutation) => {
  794. mutation.addedNodes.forEach((addedNode) => {
  795. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  796. callback(addedNode); // 执行回调函数
  797. }
  798. });
  799. });
  800. });
  801.  
  802. childObserver.observe(node, { childList: true, subtree: true, attributes: false });
  803. }
  804. }
  805.  
  806. /**
  807. * 初始化全局变量
  808. */
  809. async function initVar() {
  810. const { hostname, href } = window.location;
  811. OJBetter.state.formatName = (() => OJBetter.state.name
  812. .toLowerCase()
  813. .replace(/\s+/g, '-')
  814. .replace(/[^a-z0-9-]/g, ''))();
  815. OJBetter.state.lastAnnounceVer = OJB_getGMValue("lastAnnounceVer", "0");
  816. OJBetter.state.lastReadAnnounceVer = OJB_getGMValue("lastReadAnnounceVer", "0");
  817. OJBetter.typeOfPage.is_mSite = /^m[0-9]/.test(hostname);
  818. OJBetter.typeOfPage.is_oldLatex = $('.tex-span').length;
  819. OJBetter.typeOfPage.is_gym = href.includes("gym") && href.includes('/problem/');
  820. OJBetter.typeOfPage.is_acmsguru = href.includes("acmsguru") && href.includes('/problem/');
  821. OJBetter.typeOfPage.is_contest = /\/contest\/[\d\/\s]+$/.test(href) && !href.includes('/problem/');
  822. OJBetter.typeOfPage.is_problem = href.includes('/problem/');
  823. OJBetter.typeOfPage.is_completeProblemset = /problems\/?$/.test(href);
  824. OJBetter.typeOfPage.is_problemset_problem = href.includes('/problemset/') && href.includes('/problem/');
  825. OJBetter.typeOfPage.is_problemset = href.includes('/problemset') && !href.includes('/problem/');
  826. OJBetter.typeOfPage.is_submitPage = href.includes('/submit');
  827. OJBetter.typeOfPage.is_statePage = href.includes('/status');
  828. OJBetter.typeOfPage.is_submissions = href.includes('/submissions');
  829. OJBetter.typeOfPage.is_submission = href.includes('/submission');
  830. OJBetter.typeOfPage.is_cfStandings = href.includes('/standings') &&
  831. $('.standings tr:first th:nth-child(n+5)')
  832. .map(function () {
  833. return $(this).find('span').text();
  834. })
  835. .get()
  836. .every(score => /^[0-9]+$/.test(score));
  837. OJBetter.localization.websiteLang = OJB_getGMValue("localizationLanguage", "zh");
  838. OJBetter.localization.scriptLang = OJB_getGMValue("scriptL10nLanguage", "zh");
  839. OJBetter.basic.renderPerfOpt = OJB_getGMValue("renderPerfOpt", false);
  840. OJBetter.basic.selectElementPerfOpt = OJB_getGMValue("selectElementPerfOpt", false);
  841. OJBetter.basic.commentPaging = OJB_getGMValue("commentPaging", true);
  842. OJBetter.basic.showJumpToLuogu = OJB_getGMValue("showJumpToLuogu", true);
  843. OJBetter.basic.showCF2vjudge = OJB_getGMValue("showCF2vjudge", true);
  844. OJBetter.basic.standingsRecolor = OJB_getGMValue("standingsRecolor", true);
  845. OJBetter.basic.hiddenProblemTag = OJB_getGMValue("hiddenProblemTag", false);
  846. OJBetter.state.notWaiteLoaded = OJB_getGMValue("notWaiteLoaded", false);
  847. OJBetter.translation.targetLang = OJB_getGMValue("transTargetLang", "zh");
  848. OJBetter.translation.choice = OJB_getGMValue("translation", "deepl");
  849. OJBetter.translation.comment.transMode = OJB_getGMValue("commentTranslationMode", "0");
  850. OJBetter.translation.comment.choice = OJB_getGMValue("commentTranslationChoice", "0");
  851. OJBetter.translation.memory.enabled = OJB_getGMValue("memoryTranslateHistory", true);
  852. OJBetter.translation.auto.enabled = OJB_getGMValue("autoTranslation", false);
  853. OJBetter.translation.auto.shortTextLength = OJB_getGMValue("shortTextLength", "2000");
  854. OJBetter.translation.retransAction = OJB_getGMValue("retransAction", "0");
  855. OJBetter.translation.waitTime = OJB_getGMValue("transWaitTime", "200");
  856. OJBetter.translation.auto.mixTrans.enabled = OJB_getGMValue("allowMixTrans", true);
  857. OJBetter.translation.auto.mixTrans.servers = OJB_getGMValue("mixedTranslation", ['deepl', 'iflyrec', 'youdao', 'caiyun']);
  858. OJBetter.common.taskQueue = new TaskQueue();
  859. OJBetter.translation.replaceSymbol = OJB_getGMValue("replaceSymbol", "2");
  860. OJBetter.translation.filterTextWithoutEmphasis = OJB_getGMValue("filterTextWithoutEmphasis", false);
  861. OJBetter.translation.forceTurndownConversion = OJB_getGMValue("forceTurndownConversion", false);
  862. OJBetter.clist.enabled.contest = OJB_getGMValue("showClistRating_contest", false);
  863. OJBetter.clist.enabled.problem = OJB_getGMValue("showClistRating_problem", false);
  864. OJBetter.clist.enabled.problemset = OJB_getGMValue("showClistRating_problemset", false);
  865. OJBetter.clist.ratingHidden = OJB_getGMValue("RatingHidden", false);
  866. OJBetter.clist.authorization = OJB_getGMValue("clist_Authorization", "");
  867. //deepl
  868. OJBetter.deepl.config.type = OJB_getGMValue("deepl_type", "free");
  869. OJBetter.deepl.configs = OJB_getGMValue("deepl_config", {
  870. "choice": "",
  871. "configurations": []
  872. });
  873. if (OJBetter.deepl.configs.choice !== "" && OJBetter.deepl.configs.configurations.length !== 0) {
  874. const choice = OJBetter.deepl.configs.choice;
  875. const configuration = OJBetter.deepl.configs.configurations.find(obj => obj.name === choice);;
  876. if (configuration == undefined) {
  877. let existingConfig = GM_getValue('deepl_config');
  878. existingConfig.choice = "";
  879. GM_setValue('deepl_config', existingConfig);
  880. location.reload();
  881. }
  882. OJBetter.deepl.config.name = configuration.name;
  883. OJBetter.deepl.config.apiGenre = configuration.apiGenre;
  884. OJBetter.deepl.config.key = configuration.key;
  885. OJBetter.deepl.config.proxy = configuration.proxy;
  886. OJBetter.deepl.config.header = OJB_parseLinePairArray(configuration._header);
  887. OJBetter.deepl.config.data = OJB_parseLinePairArray(configuration._data);
  888. OJBetter.deepl.config.quota.url = configuration.quota_url;
  889. OJBetter.deepl.config.quota.method = configuration.quota_method;
  890. OJBetter.deepl.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  891. OJBetter.deepl.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  892. OJBetter.deepl.config.quota.surplus = configuration.quota_surplus;
  893. }
  894. OJBetter.deepl.enableEmphasisProtection = OJB_getGMValue("enableEmphasisProtection", true);
  895. OJBetter.deepl.enableLinkProtection = OJB_getGMValue("enableLinkProtection", true);
  896. //openai
  897. OJBetter.chatgpt.isStream = OJB_getGMValue("openai_isStream", true);
  898. OJBetter.chatgpt.customPrompt = OJB_getGMValue("openai_customPrompt", '');
  899. OJBetter.chatgpt.asSystemPrompt = OJB_getGMValue("openai_asSystemPrompt", false);
  900. OJBetter.chatgpt.configs = OJB_getGMValue("chatgpt_config", {
  901. "choice": "",
  902. "configurations": []
  903. });
  904. if (OJBetter.chatgpt.configs.choice !== "" && OJBetter.chatgpt.configs.configurations.length !== 0) {
  905. const choice = OJBetter.chatgpt.configs.choice;
  906. const configuration = OJBetter.chatgpt.configs.configurations.find(obj => obj.name === choice);;
  907. if (configuration == undefined) {
  908. let existingConfig = GM_getValue('chatgpt_config');
  909. existingConfig.choice = "";
  910. GM_setValue('chatgpt_config', existingConfig);
  911. location.reload();
  912. }
  913. OJBetter.chatgpt.config.name = configuration.name;
  914. OJBetter.chatgpt.config.model = configuration.model;
  915. OJBetter.chatgpt.config.key = configuration.key;
  916. OJBetter.chatgpt.config.proxy = configuration.proxy;
  917. OJBetter.chatgpt.config.header = OJB_parseLinePairArray(configuration._header);
  918. OJBetter.chatgpt.config.data = OJB_parseLinePairArray(configuration._data);
  919. OJBetter.chatgpt.config.quota.url = configuration.quota_url;
  920. OJBetter.chatgpt.config.quota.method = configuration.quota_method;
  921. OJBetter.chatgpt.config.quota.header = OJB_parseLinePairArray(configuration.quota_header);
  922. OJBetter.chatgpt.config.quota.data = OJB_parseLinePairArray(configuration.quota_data);
  923. OJBetter.chatgpt.config.quota.surplus = configuration.quota_surplus;
  924. }
  925. // 编辑器
  926. if (!OJBetter.typeOfPage.is_mSite) OJBetter.common.cf_csrf_token = Codeforces.getCsrfToken();
  927. else OJBetter.common.cf_csrf_token = "";
  928. OJBetter.monaco.compilerSelection = OJB_getGMValue("compilerSelection", "61");
  929. OJBetter.monaco.setting.fontsize = OJB_getGMValue("editorFontSize", "15");
  930. OJBetter.monaco.enableOnProblemPage = OJB_getGMValue("problemPageCodeEditor", true);
  931. OJBetter.monaco.beautifyPreBlocks = OJB_getGMValue("beautifyPreBlocks", true);
  932. OJBetter.monaco.complet.cppCodeTemplate = OJB_getGMValue("cppCodeTemplateComplete", true);
  933. OJBetter.monaco.onlineCompilerChoice = OJB_getGMValue("onlineCompilerChoice", "official");
  934. OJBetter.monaco.setting.isCodeSubmitDoubleConfirm = OJB_getGMValue("isCodeSubmitConfirm", true);
  935. OJBetter.monaco.setting.autoSubmitAfterPass = OJB_getGMValue("autoSubmitAfterPass", false);
  936. OJBetter.monaco.setting.alwaysConsumeMouseWheel = OJB_getGMValue("alwaysConsumeMouseWheel", true);
  937. OJBetter.monaco.setting.submitButtonPosition = OJB_getGMValue("submitButtonPosition", "bottom");
  938. OJBetter.monaco.setting.autoMemoryCode = OJB_getGMValue("autoMemoryCode", true);
  939. //自定义补全
  940. OJBetter.monaco.complet.customConfig = OJB_getGMValue("Complet_config", {
  941. "choice": -1,
  942. "configurations": []
  943. });
  944. // monaco
  945. OJBetter.monaco.lsp.enabled = OJB_getGMValue("useLSP", false);
  946. OJBetter.monaco.setting.position = OJB_getGMValue("monacoEditor_position", "initial");
  947. OJBetter.monaco.lsp.workUri = OJB_getGMValue("OJBetter_Bridge_WorkUri", "C:/OJBetter_Bridge");
  948. OJBetter.monaco.lsp.socketUrl = OJB_getGMValue("OJBetter_Bridge_SocketUrl", "ws://127.0.0.1:2323/");
  949. OJBetter.preference.showLoading = OJB_getGMValue("showLoading", true);
  950. OJBetter.preference.hoverTargetAreaDisplay = OJB_getGMValue("hoverTargetAreaDisplay", false);
  951. OJBetter.basic.expandFoldingblocks = OJB_getGMValue("expandFoldingblocks", true);
  952. OJBetter.preference.iconButtonSize = OJB_getGMValue("iconButtonSize", "16");
  953. OJBetter.dev.isRuleMarkingEnabled = OJB_getGMValue("isRuleMarkingEnabled", false);
  954. OJBetter.about.updateChannel = OJB_getGMValue("updateChannel", "release");
  955. OJBetter.about.updateSource = OJB_getGMValue("updateSource", "greasyfork");
  956. }
  957.  
  958. /**
  959. * 显示警告消息
  960. */
  961. function showWarnMessage() {
  962. if (OJBetter.typeOfPage.is_oldLatex) {
  963. const loadingMessage = new LoadingMessage();
  964. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_oldLatex', { ns: 'alert' })}`, 'warning');
  965. }
  966. if (OJBetter.typeOfPage.is_acmsguru) {
  967. const loadingMessage = new LoadingMessage();
  968. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_acmsguru', { ns: 'alert' })}`, 'warning');
  969. }
  970. if (OJBetter.translation.comment.transMode == "1") {
  971. const loadingMessage = new LoadingMessage();
  972. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_segment', { ns: 'alert' })}`, 'warning');
  973. }
  974. if (OJBetter.translation.comment.transMode == "2") {
  975. const loadingMessage = new LoadingMessage();
  976. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.trans_select', { ns: 'alert' })}`, 'warning');
  977. }
  978. if (OJBetter.typeOfPage.is_submitPage && OJBetter.monaco.enableOnProblemPage) {
  979. const loadingMessage = new LoadingMessage();
  980. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.is_submitPage', { ns: 'alert' })}`, 'warning');
  981. }
  982. if (OJBetter.translation.forceTurndownConversion) {
  983. const loadingMessage = new LoadingMessage();
  984. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('warning.forceTurndownConversion', { ns: 'alert' })}`, 'warning');
  985. }
  986. }
  987.  
  988. // 常量
  989. const helpCircleHTML = '<div class="help-icon"><svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm23.744 191.488c-52.096 0-92.928 14.784-123.2 44.352-30.976 29.568-45.76 70.4-45.76 122.496h80.256c0-29.568 5.632-52.8 17.6-68.992 13.376-19.712 35.2-28.864 66.176-28.864 23.936 0 42.944 6.336 56.32 19.712 12.672 13.376 19.712 31.68 19.712 54.912 0 17.6-6.336 34.496-19.008 49.984l-8.448 9.856c-45.76 40.832-73.216 70.4-82.368 89.408-9.856 19.008-14.08 42.24-14.08 68.992v9.856h80.96v-9.856c0-16.896 3.52-31.68 10.56-45.76 6.336-12.672 15.488-24.64 28.16-35.2 33.792-29.568 54.208-48.576 60.544-55.616 16.896-22.528 26.048-51.392 26.048-86.592 0-42.944-14.08-76.736-42.24-101.376-28.16-25.344-65.472-37.312-111.232-37.312zm-12.672 406.208a54.272 54.272 0 0 0-38.72 14.784 49.408 49.408 0 0 0-15.488 38.016c0 15.488 4.928 28.16 15.488 38.016A54.848 54.848 0 0 0 523.072 768c15.488 0 28.16-4.928 38.72-14.784a51.52 51.52 0 0 0 16.192-38.72 51.968 51.968 0 0 0-15.488-38.016 55.936 55.936 0 0 0-39.424-14.784z"></path></svg></div>';
  990. const closeIcon = `<svg t="1696693011050" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4322" width="14" height="14"><path d="M0 0h1024v1024H0z" fill-opacity="0" p-id="4323"></path><path d="M240.448 168l2.346667 2.154667 289.92 289.941333 279.253333-279.253333a42.666667 42.666667 0 0 1 62.506667 58.026666l-2.133334 2.346667-279.296 279.210667 279.274667 279.253333a42.666667 42.666667 0 0 1-58.005333 62.528l-2.346667-2.176-279.253333-279.253333-289.92 289.962666a42.666667 42.666667 0 0 1-62.506667-58.005333l2.154667-2.346667 289.941333-289.962666-289.92-289.92a42.666667 42.666667 0 0 1 57.984-62.506667z" p-id="4324"></path></svg>`;
  991. const translateIcon = `<svg t="1696837407077" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6325" width="22" height="22"><path d="M536.380952 121.904762a73.142857 73.142857 0 0 1 73.142858 73.142857v219.428571h219.428571a73.142857 73.142857 0 0 1 73.142857 73.142858v341.333333a73.142857 73.142857 0 0 1-73.142857 73.142857H487.619048a73.142857 73.142857 0 0 1-73.142858-73.142857v-219.428571H195.047619a73.142857 73.142857 0 0 1-73.142857-73.142858V195.047619a73.142857 73.142857 0 0 1 73.142857-73.142857h341.333333zM243.809524 682.666667v97.523809h97.523809v73.142857h-97.523809a73.142857 73.142857 0 0 1-73.142857-73.142857v-97.523809h73.142857z m585.142857-195.047619h-219.428571v48.761904a73.142857 73.142857 0 0 1-73.142858 73.142858h-48.761904v219.428571h341.333333V487.619048z m-115.760762 89.526857L787.21219 780.190476h-62.025142l-14.043429-42.715428h-76.068571L620.739048 780.190476h-60.854858l74.605715-203.044571h78.701714z m-38.034286 50.029714h-3.510857l-21.065143 63.488h45.348572l-20.772572-63.488zM536.380952 195.047619H195.047619v341.333333h341.333333V195.047619z
  992. m-195.072 49.883429l44.78781 1.072762v37.278476h87.698286v145.359238h-87.698286v65.974857h-44.78781v-65.974857h-87.698285v-145.359238h87.698285v-38.351238z m0 83.139047h-44.787809v56.05181h44.787809v-56.05181z m89.307429 0h-44.519619v56.05181h44.519619v-56.05181zM780.190476 170.666667a73.142857 73.142857 0 0 1 73.142857 73.142857v97.523809h-73.142857v-97.523809h-97.523809V170.666667h97.523809z" p-id="6326"></path></svg>`;
  993. const clistIcon = `<svg width="37.7pt" height="10pt" viewBox="0 0 181 48" version="1.1" xmlns="http://www.w3.org/2000/svg"><g id="#0057b8ff"><path fill="#0057b8" opacity="1.00" d=" M 17.36 0.00 L 18.59 0.00 C 23.84 6.49 30.28 11.92 36.01 17.98 C 34.01 19.99 32.01 21.99 30.00 23.99 C 26.02 19.97 22.02 15.98 18.02 11.99 C 14.01 15.98 10.01 19.99 6.00 23.99 C 4.16 22.04 2.30 20.05 0.00 18.61 L 0.00 17.37 C 3.44 15.11 6.00 11.84 8.96 9.03 C 11.79 6.05 15.09 3.47 17.36 0.00 Z" /></g><g id="#a0a0a0ff"><path fill="#a0a0a0" opacity="1.00" d=" M 56.76 13.74 C 61.48 4.80 76.07 3.90 81.77 12.27 C 83.09 13.94 83.44 16.10 83.91 18.12 C 81.53 18.23 79.16 18.24 76.78 18.23 C 75.81 15.72 73.99 13.31 71.14 12.95 C 67.14 12.02 63.45 15.29 62.48 18.99 C 61.30 23.27 61.71 28.68 65.34 31.70 C 67.82 34.05 72.19 33.93 74.61 31.55 C 75.97 30.18 76.35 28.23 76.96 26.48 C 79.36 26.43 81.77 26.44 84.17 26.56 C 83.79 30.09 82.43 33.49 79.89 36.02 C 74.14 41.35 64.17 40.80 58.77 35.25 C 53.52 29.56 53.18 20.38 56.76 13.74 Z" />
  994. <path fill="#a0a0a0" opacity="1.00" d=" M 89.01 7.20 C 91.37 7.21 93.74 7.21 96.11 7.22 C 96.22 15.71 96.10 24.20 96.18 32.69 C 101.25 32.76 106.32 32.63 111.39 32.79 C 111.40 34.86 111.41 36.93 111.41 39.00 C 103.94 39.00 96.47 39.00 89.00 39.00 C 89.00 28.40 88.99 17.80 89.01 7.20 Z" /><path fill="#a0a0a0" opacity="1.00" d=" M 115.00 7.21 C 117.33 7.21 119.66 7.21 121.99 7.21 C 122.01 17.81 122.00 28.40 122.00 39.00 C 119.67 39.00 117.33 39.00 115.00 39.00 C 115.00 28.40 114.99 17.80 115.00 7.21 Z" /><path fill="#a0a0a0" opacity="1.00" d=" M 133.35 7.47 C 139.11 5.56 146.93 6.28 150.42 11.87 C 151.42 13.39 151.35 15.31 151.72 17.04 C 149.33 17.05 146.95 17.05 144.56 17.03 C 144.13 12.66 138.66 11.12 135.34 13.30 C 133.90 14.24 133.54 16.87 135.35 17.61 C 139.99 20.02 145.90 19.54 149.92 23.19 C 154.43 26.97 153.16 35.36 147.78 37.72 C 143.39 40.03 137.99 40.11 133.30 38.69 C 128.80 37.34 125.34 32.90 125.91 28.10 C 128.22 28.10 130.53 28.11 132.84 28.16 C 132.98 34.19 142.68 36.07 145.18 30.97 C 146.11 27.99 142.17 27.05 140.05 26.35 C 135.54 25.04 129.83 24.33 127.50 19.63 C 125.30 14.78 128.42 9.00 133.35 7.47 Z" />
  995. <path fill="#a0a0a0" opacity="1.00" d=" M 153.31 7.21 C 161.99 7.21 170.67 7.21 179.34 7.21 C 179.41 9.30 179.45 11.40 179.48 13.50 C 176.35 13.50 173.22 13.50 170.09 13.50 C 170.05 21.99 170.12 30.48 170.05 38.98 C 167.61 39.00 165.18 39.00 162.74 39.00 C 162.64 30.52 162.73 22.04 162.69 13.55 C 159.57 13.49 156.44 13.49 153.32 13.50 C 153.32 11.40 153.31 9.31 153.31 7.21 Z" /></g><g id="#ffd700ff"><path fill="#ffd700" opacity="1.00" d=" M 12.02 29.98 C 14.02 27.98 16.02 25.98 18.02 23.98 C 22.01 27.99 26.03 31.97 30.00 35.99 C 34.01 31.99 38.01 27.98 42.02 23.99 C 44.02 25.98 46.02 27.98 48.01 29.98 C 42.29 36.06 35.80 41.46 30.59 48.00 L 29.39 48.00 C 24.26 41.42 17.71 36.08 12.02 29.98 Z" /></g></svg>`;
  996.  
  997. /**
  998. * 连接数据库
  999. */
  1000. async function initDB() {
  1001. OJBetter.common.database = new Dexie('CFBetterDB');
  1002. OJBetter.common.database.version(3).stores({
  1003. samplesData: '&url',
  1004. editorCode: '&url',
  1005. translateData: '&url',
  1006. localizeSubsData: '&lang'
  1007. });
  1008.  
  1009. // 等待数据库打开
  1010. await OJBetter.common.database.open();
  1011. }
  1012.  
  1013. /**
  1014. * 清空数据库
  1015. */
  1016. async function clearDatabase() {
  1017. const isConfirmed = await OJB_createDialog(
  1018. i18next.t('isClearDatabase.title', { ns: 'dialog' }),
  1019. i18next.t('isClearDatabase.content', { ns: 'dialog' }),
  1020. [
  1021. i18next.t('isClearDatabase.buttons.0', { ns: 'dialog' }),
  1022. i18next.t('isClearDatabase.buttons.1', { ns: 'dialog' })
  1023. ]
  1024. );
  1025. if (!isConfirmed) {
  1026. try {
  1027. // 开启一个读写事务,包含数据库中的所有表
  1028. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  1029. // 遍历所有表
  1030. for (const table of OJBetter.common.database.tables) {
  1031. // 清空当前表
  1032. await table.clear();
  1033. }
  1034. });
  1035. console.log("All tables in the database have been cleared.");
  1036. alert("All tables in the database have been cleared.");
  1037. } catch (error) {
  1038. console.error("Error clearing the database:", error);
  1039. }
  1040. }
  1041. }
  1042.  
  1043. /**
  1044. * 导出数据库
  1045. * @returns {Promise<string>} 数据库的JSON字符串
  1046. */
  1047. async function exportDatabase() {
  1048. try {
  1049. // 创建一个存储数据的对象
  1050. const exportData = {};
  1051. // 获取数据库中所有表的名称
  1052. const tableNames = OJBetter.common.database.tables.map(table => table.name);
  1053.  
  1054. // 遍历每一个表,获取数据
  1055. for (const tableName of tableNames) {
  1056. const tableData = await OJBetter.common.database.table(tableName).toArray();
  1057. exportData[tableName] = tableData;
  1058. }
  1059.  
  1060. // 将数据对象转换为JSON字符串
  1061. const jsonData = JSON.stringify(exportData, null, 4);
  1062. return jsonData;
  1063. } catch (error) {
  1064. console.error("Error exporting database:", error);
  1065. }
  1066. }
  1067.  
  1068. /**
  1069. * 导入数据库
  1070. * @param {string} jsonData 数据库的JSON字符串
  1071. */
  1072. async function importDatabase(jsonData) {
  1073. const isConfirmed = await OJB_createDialog(
  1074. i18next.t('isImportDatabase.title', { ns: 'dialog' }),
  1075. i18next.t('isImportDatabase.content', { ns: 'dialog' }),
  1076. [
  1077. i18next.t('isImportDatabase.buttons.0', { ns: 'dialog' }),
  1078. i18next.t('isImportDatabase.buttons.1', { ns: 'dialog' })
  1079. ]
  1080. );
  1081. if (!isConfirmed) {
  1082. try {
  1083. // 将JSON字符串解析为对象
  1084. const importData = JSON.parse(jsonData);
  1085.  
  1086. // 开启一个事务,并清空现有数据
  1087. await OJBetter.common.database.transaction('rw', OJBetter.common.database.tables, async () => {
  1088. // 清空所有表的数据
  1089. for (const tableName of OJBetter.common.database.tables.map(table => table.name)) {
  1090. await OJBetter.common.database.table(tableName).clear();
  1091. }
  1092.  
  1093. // 插入新数据
  1094. for (const [tableName, rows] of Object.entries(importData)) {
  1095. await OJBetter.common.database.table(tableName).bulkAdd(rows);
  1096. }
  1097. });
  1098. alert("Data imported successfully");
  1099. } catch (error) {
  1100. console.error("Error importing database:", error);
  1101. }
  1102. }
  1103. }
  1104.  
  1105. /**
  1106. * 将数据下载为文件
  1107. * @param {string} data 数据
  1108. * @param {string} filename 文件名,默认为'export.json'
  1109. * @param {string} fileType 文件MIME类型,默认为'application/json'
  1110. * @returns {void}
  1111. */
  1112. function downloadDataAsFile(data, filename = 'export.json', fileType = 'application/json') {
  1113. // 创建一个blob对象,指定文件类型
  1114. const blob = new Blob([data], { type: fileType });
  1115. const url = URL.createObjectURL(blob);
  1116.  
  1117. // 创建一个隐藏的a标签,模拟点击进行下载
  1118. const a = document.createElement('a');
  1119. a.href = url;
  1120. a.download = filename;
  1121. document.body.appendChild(a);
  1122. a.click();
  1123.  
  1124. // 清理
  1125. document.body.removeChild(a);
  1126. URL.revokeObjectURL(url);
  1127. }
  1128.  
  1129.  
  1130. /**
  1131. * 从文件中读取数据
  1132. * @param {Function} callback 回调函数
  1133. * @returns {void}
  1134. */
  1135. function readFileInput(callback) {
  1136. const fileInput = document.createElement('input');
  1137. fileInput.type = 'file';
  1138. fileInput.accept = '.json';
  1139. fileInput.style.display = 'none'; // 隐藏input元素
  1140.  
  1141. fileInput.onchange = (e) => {
  1142. const file = e.target.files[0];
  1143. if (file) {
  1144. const reader = new FileReader();
  1145. reader.onload = (e) => {
  1146. const fileContent = e.target.result;
  1147. if (callback && typeof callback === 'function') {
  1148. callback(fileContent); // 调用回调函数,传入文件内容
  1149. }
  1150. };
  1151. reader.readAsText(file);
  1152. }
  1153. };
  1154.  
  1155. document.body.appendChild(fileInput);
  1156. fileInput.click();
  1157. document.body.removeChild(fileInput);
  1158. }
  1159.  
  1160. /**
  1161. * 清除所有设置
  1162. */
  1163. async function deleteAllConfigSettings() {
  1164. const isConfirmed = await OJB_createDialog(
  1165. i18next.t('isDeleteAllConfigSettings.title', { ns: 'dialog' }),
  1166. i18next.t('isDeleteAllConfigSettings.content', { ns: 'dialog' }),
  1167. [
  1168. i18next.t('isDeleteAllConfigSettings.buttons.0', { ns: 'dialog' }),
  1169. i18next.t('isDeleteAllConfigSettings.buttons.1', { ns: 'dialog' })
  1170. ]
  1171. );
  1172. if (!isConfirmed) {
  1173. const keys = GM_listValues();
  1174.  
  1175. keys.forEach(key => {
  1176. GM_deleteValue(key);
  1177. });
  1178.  
  1179. alert('All settings have been deleted.');
  1180. window.location.reload();
  1181. }
  1182. }
  1183.  
  1184. /**
  1185. * 导出设置到JSON
  1186. * @returns {string} JSON字符串
  1187. */
  1188. function exportSettingsToJSON() {
  1189. const keys = GM_listValues();
  1190. let settings = {};
  1191.  
  1192. keys.forEach(key => {
  1193. settings[key] = GM_getValue(key);
  1194. });
  1195.  
  1196. return JSON.stringify(settings, null, 4);
  1197. }
  1198.  
  1199. /**
  1200. * 从JSON导入设置
  1201. * @param {string} jsonData JSON字符串
  1202. * @returns {void}
  1203. */
  1204. async function importSettingsFromJSON(jsonData) {
  1205. const isConfirmed = await OJB_createDialog(
  1206. i18next.t('isImportSettings.title', { ns: 'dialog' }),
  1207. i18next.t('isImportSettings.content', { ns: 'dialog' }),
  1208. [
  1209. i18next.t('isImportSettings.buttons.0', { ns: 'dialog' }),
  1210. i18next.t('isImportSettings.buttons.1', { ns: 'dialog' })
  1211. ]
  1212. );
  1213. if (!isConfirmed) {
  1214. let settings;
  1215. try {
  1216. settings = JSON.parse(jsonData);
  1217. } catch (e) {
  1218. console.error('JSON parsing error:', e);
  1219. return;
  1220. }
  1221.  
  1222. Object.keys(settings).forEach(key => {
  1223. GM_setValue(key, settings[key]);
  1224. });
  1225.  
  1226. alert('Settings imported successfully!');
  1227. window.location.reload();
  1228. }
  1229. }
  1230.  
  1231. /**
  1232. * 加载元素本地化语言数据
  1233. * @param {JQuery} element jQuery元素
  1234. * @param {number} [retries=10] 重试次数
  1235. * @param {number} [interval=50] 重试间隔
  1236. */
  1237. function elementLocalize(element, retries = 10, interval = 50) {
  1238. if ($.isFunction(element.localize)) {
  1239. element.localize();
  1240. } else if (retries > 0) {
  1241. setTimeout(elementLocalize, interval, element, retries - 1, interval);
  1242. } else {
  1243. console.error('Unable to localize', element);
  1244. }
  1245. }
  1246.  
  1247. // 切换系统黑暗监听
  1248. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  1249. const changeEventListeners = [];
  1250. function handleColorSchemeChange(event) {
  1251. event.matches ? $('html').attr('data-theme', 'dark') : $('html').attr('data-theme', 'light');
  1252. if (!event.matches) {
  1253. var originalColor = $(this).data("original-color");
  1254. $(this).css("background-color", originalColor);
  1255. const intervalId = setinterval(() => {
  1256. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1257. monaco.editor.setTheme('vs');
  1258. clearInterval(intervalId);
  1259. }
  1260. }, 100);
  1261. } else {
  1262. const intervalId = setInterval(() => {
  1263. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1264. monaco.editor.setTheme('vs-dark');
  1265. clearInterval(intervalId);
  1266. }
  1267. }, 100);
  1268. }
  1269. }
  1270.  
  1271. /**
  1272. * 黑暗模式
  1273. */
  1274. (function setDark() {
  1275. /**
  1276. * 初始化
  1277. */
  1278. function setDarkTheme() {
  1279. const htmlElement = document.querySelector('html');
  1280. if (htmlElement) {
  1281. htmlElement.setAttribute('data-theme', 'dark');
  1282. const intervalId = setInterval(() => {
  1283. if (OJBetter.monaco && OJBetter.monaco.editor) {
  1284. monaco.editor.setTheme('vs-dark');
  1285. clearInterval(intervalId);
  1286. }
  1287. }, 100);
  1288. } else {
  1289. setTimeout(setDarkTheme, 100);
  1290. }
  1291. }
  1292. OJBetter.basic.darkMode = OJB_getGMValue("darkMode", "follow")
  1293. if (OJBetter.basic.darkMode == "dark") {
  1294. setDarkTheme();
  1295. } else if (OJBetter.basic.darkMode == "follow") {
  1296. // 添加事件监听器
  1297. changeEventListeners.push(handleColorSchemeChange);
  1298. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  1299.  
  1300. if (window.matchMedia('(prefers-color-scheme: dark)').matches) setDarkTheme();
  1301. }
  1302.  
  1303. // 定义全局变量
  1304. GM_addStyle(`
  1305. /* 黑暗支持 */
  1306. html[data-theme=dark]:root {
  1307. color-scheme: light dark;
  1308. }
  1309. /* 颜色 */
  1310. :root {
  1311. /* 文字颜色 */
  1312. --ojb-color-text-primary: #a0adb9; /* 主要文字颜色 */
  1313. --ojb-color-text-secondary: #9AA4B1; /* 次要文字颜色 */
  1314. --ojb-color-text-tertiary: #9BA5B2; /* 第三级文字颜色 */
  1315. --ojb-color-text-success: #43A047; /* 成功状态文字颜色 */
  1316. --ojb-color-text-highlight: #cbd6e2; /* 高亮文字颜色 */
  1317. --ojb-color-text-disabled: #506778; /* 禁用状态文字颜色 */
  1318. --ojb-color-text-icon-success: #2e7d32; /* 成功状态图标颜色 */
  1319. --ojb-color-text-link: #4b8eda; /* 链接颜色 */
  1320.  
  1321. /* 背景颜色 */
  1322. --ojb-color-bg-primary: #22272e; /* 主背景颜色 */
  1323. --ojb-color-bg-secondary: #2d333b; /* 次级背景颜色 */
  1324. --ojb-color-bg-disabled: #24292e; /* 禁用元素背景颜色 */
  1325.  
  1326. /* 边框颜色 */
  1327. --ojb-color-border-primary: #48535F; /* 主要边框颜色 */
  1328. --ojb-color-border-disabled: #404950; /* 禁用状态边框颜色 */
  1329. --ojb-color-border-dashed-hover: #03A9F4; /* 虚线边框悬浮颜色 */
  1330. --ojb-color-border-radio-checked: #326154; /* 选中的单选框边框颜色 */
  1331.  
  1332. /* 阴影颜色 */
  1333. --ojb-shadow-standard: 0px 0px 0.5px 0.5px #3A4048; /* 标准阴影 */
  1334. --ojb-shadow-menu-modal: 0px 0px 0px 4px #2d333b; /* 菜单和模态框阴影 */
  1335.  
  1336. /* 区域遮罩颜色 */
  1337. --ojb-overlay-background: repeating-linear-gradient(135deg, #49525f6e, #49525f6e 30px, #49525f29 0px, #49525f29 55px); /* 区域遮罩背景 */
  1338.  
  1339. /* 文字阴影 */
  1340. --ojb-text-shadow-icon: 1px 1px 0px #2d333b, 1px -1px 0px #2d333b, -1px -1px 0px #2d333b, -1px 1px 0px #2d333b; /* 图标文字阴影 */
  1341. }
  1342. /* 边框样式 */
  1343. :root {
  1344. /* 边框样式 */
  1345. --ojb-border-width: 1px; /* 边框宽度 */
  1346. --ojb-border-style-solid: solid; /* 实线样式 */
  1347. --ojb-border-style-dashed: dashed; /* 虚线样式 */
  1348. --ojb-border-radius-small: 4px; /* 小圆角 */
  1349. --ojb-border-radius-medium: 8px; /* 中圆角 */
  1350. --ojb-border-radius-large: 12px; /* 大圆角 */
  1351.  
  1352. /* 组合边框样式 */
  1353. --ojb-border-solid-primary: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-primary); /* 主要实线边框 */
  1354. --ojb-border-dashed: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-primary); /* 主要虚线边框 */
  1355. --ojb-border-dashed-hover: var(--ojb-border-width) var(--ojb-border-style-dashed) var(--ojb-color-border-dashed-hover); /* 悬浮虚线边框 */
  1356. --ojb-border-solid-disabled: var(--ojb-border-width) var(--ojb-border-style-solid) var(--ojb-color-border-disabled); /* 禁用状态实线边框 */
  1357. }
  1358. `);
  1359.  
  1360. // OJBetter界面样式
  1361. GM_addStyle(`
  1362. /* 主要文字颜色 */
  1363. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1364. html[data-theme=dark] .alert-warning, html[data-theme=dark] .markItUpEditor,
  1365. html[data-theme=dark] .translate-problem-statement, html[data-theme=dark] .OJBetter_setting_menu,
  1366. html[data-theme=dark] .help_tip .tip_text,
  1367. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1368. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1369. html[data-theme=dark] #items-per-page, html[data-theme=dark] #pagBar,
  1370. html[data-theme=dark] .OJBetter_setting_sidebar li a:link,
  1371. html[data-theme=dark] .popup .content{
  1372. color: var(--ojb-color-text-primary);
  1373. }
  1374. /* 次要文字颜色 */
  1375. html[data-theme=dark] .ojb_btn:hover, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_statusBar,
  1376. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #addCustomTest,
  1377. html[data-theme=dark] #customTestBlock, html[data-theme=dark] .OJBetter_setting_list.alert_info{
  1378. color: var(--ojb-color-text-secondary);
  1379. }
  1380. /* 文字颜色3 */
  1381. html[data-theme=dark] .ojb_btn{
  1382. color: var(--ojb-color-text-tertiary);
  1383. }
  1384. /* 文字颜色 浅绿 */
  1385. html[data-theme=dark] #SubmitButton{
  1386. color: var(--ojb-color-text-success);
  1387. }
  1388. /* 禁止文字颜色 */
  1389. html[data-theme=dark] .ojb_btn[disabled]{
  1390. color: var(--ojb-color-text-disabled);
  1391. }
  1392. /* 主要背景层次 */
  1393. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] li#add_button:hover,
  1394. html[data-theme=dark] .ojb_btn:hover,
  1395. html[data-theme=dark] .OJBetter_setting_menu input, html[data-theme=dark] .OJBetter_setting_menu textarea,
  1396. html[data-theme=dark] #OJBetter_SubmitForm input,
  1397. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1398. html[data-theme=dark] #OJBetter_SubmitForm textarea, html[data-theme=dark] #OJBetter_SubmitForm select,
  1399. html[data-theme=dark] .OJBetter_setting_sidebar li a.active, html[data-theme=dark] .OJBetter_setting_sidebar li,
  1400. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-track, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-track,
  1401. html[data-theme=dark] .OJBetter_modal, html[data-theme=dark] .OJBetter_modal button:hover,
  1402. html[data-theme=dark] .popup .content,
  1403. html[data-theme=dark] .config_bar_list, html[data-theme=dark] #LSPLog,
  1404. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1405. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before,
  1406. html[data-theme=dark] .OJBetter_setting_menu a, html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button:hover,
  1407. html[data-theme=dark] .OJBetter_setting_menu select{
  1408. background-color: var(--ojb-color-bg-primary);
  1409. background-image: none;
  1410. }
  1411. /* 次要背景层次 */
  1412. html[data-theme=dark] .ojb_btn,
  1413. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1414. html[data-theme=dark] .alert-warning, html[data-theme=dark] .SumoSelect>.optWrapper>.options li.opt:hover,
  1415. html[data-theme=dark] .translate-problem-statement-panel,
  1416. html[data-theme=dark] .translate-problem-statement,
  1417. html[data-theme=dark] .OJBetter_setting_list,
  1418. html[data-theme=dark] .OJBetter_setting_menu hr,
  1419. html[data-theme=dark] .OJBetter_setting_sidebar li a,
  1420. html[data-theme=dark] .OJBetter_setting_menu::-webkit-scrollbar-thumb, html[data-theme=dark] .OJBetter_setting_content::-webkit-scrollbar-thumb,
  1421. html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] .test-for-popup pre,
  1422. html[data-theme=dark] .popup .content pre, html[data-theme=dark] .popup .content pre code,
  1423. html[data-theme=dark] ul.config_bar_ul::-webkit-scrollbar-thumb, html[data-theme=dark] #OJBetter_statusBar,
  1424. html[data-theme=dark] #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] .sampleDiv,
  1425. html[data-theme=dark] #addCustomTest, html[data-theme=dark] #LSPLog li:nth-child(odd),
  1426. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before,
  1427. html[data-theme=dark] .config::before, html[data-theme=dark] .config li.tempConfig_add_button:hover,
  1428. html[data-theme=dark] .OJBetter_setting_menu details, html[data-theme=dark] #config_bar_menu,
  1429. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_setting_list button,
  1430. html[data-theme=dark] .OJBetter_setting_menu .badge, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton{
  1431. background-color: var(--ojb-color-bg-secondary);
  1432. }
  1433. /* 禁止背景层次 */
  1434. html[data-theme=dark] .ojb_btn[disabled]{
  1435. background-color: var(--ojb-color-bg-disabled);
  1436. }
  1437. /* 实线边框颜色-圆角 */
  1438. html[data-theme=dark] .alert-success, html[data-theme=dark] .alert-info, html[data-theme=dark] .alert-error,
  1439. html[data-theme=dark] .alert-warning, html[data-theme=dark] .translate-problem-statement{
  1440. border: var(--ojb-border-solid-primary);
  1441. border-radius: 2px;
  1442. }
  1443. /* 实线边框颜色-无圆角 */
  1444. html[data-theme=dark] .ojb_btn,
  1445. html[data-theme=dark] .OJBetter_setting_list, html[data-theme=dark] .config_bar_list,
  1446. html[data-theme=dark] label.config_bar_ul_li_text,
  1447. html[data-theme=dark] .OJBetter_setting_sidebar li, html[data-theme=dark] .OJBetter_setting_menu select,
  1448. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .OJBetter_modal button, html[data-theme=dark] #OJBetter_SubmitForm select,
  1449. html[data-theme=dark] #OJBetter_editor, html[data-theme=dark] #OJBetter_statusBar,
  1450. html[data-theme=dark] #OJBetter_SubmitForm #RunTestButton, html[data-theme=dark] #programTypeId, html[data-theme=dark] #customTestBlock,
  1451. html[data-theme=dark] #OJBetter_SubmitForm #addCustomTest, html[data-theme=dark] #OJBetter_SubmitForm #SubmitButton,
  1452. html[data-theme=dark] .OJBetter_setting_menu input,
  1453. html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"], html[data-theme=dark] .OJBetter_setting_menu input[type="checkbox"]:checked,
  1454. html[data-theme=dark] .OJBetter_setting_menu textarea,
  1455. html[data-theme=dark] #OJBetter_SubmitForm input, html[data-theme=dark] #OJBetter_SubmitForm textarea,
  1456. html[data-theme=dark] #CompilerSetting select, html[data-theme=dark] #CompilerSetting textarea, html[data-theme=dark] #CompilerBox,
  1457. html[data-theme=dark] .OJBetter_setting_menu .OJBetter_checkboxs,
  1458. html[data-theme=dark] .help_tip .tip_text, html[data-theme=dark] .config::before,
  1459. html[data-theme=dark] #statePanel, html[data-theme=dark] .test-case, html[data-theme=dark] .OJBetter_setting_menu .badge{
  1460. border: var(--ojb-border-solid-primary);
  1461. }
  1462. html[data-theme=dark] #customTestBlock #customTests{
  1463. border-top: var(--ojb-border-solid-primary);
  1464. }
  1465. html[data-theme=dark] .OJBetter_setting_sidebar {
  1466. border-right: var(--ojb-border-solid-primary);
  1467. }
  1468. /* 实线边框-禁止 */
  1469. html[data-theme=dark] .ojb_btn[disabled]{
  1470. border: var(--ojb-border-solid-disabled);
  1471. }
  1472. /* 虚线边框 */
  1473. html[data-theme=dark] li#add_button,
  1474. html[data-theme=dark] .OJBetter_setting_menu_label_text{
  1475. border: var(--ojb-border-dashed);
  1476. }
  1477. /* 虚线边框-悬浮 */
  1478. html[data-theme=dark] li#add_button:hover{
  1479. border: var(--ojb-border-dashed-hover);
  1480. background-color: var(--ojb-color-bg-secondary);
  1481. color: var(--ojb-color-border-dashed-hover);
  1482. }
  1483. /* 无边框 */
  1484. html[data-theme=dark] .translate-problem-statement-panel .ojb_btn{
  1485. border: none;
  1486. }
  1487. /* 区域遮罩 */
  1488. html[data-theme=dark] .overlay::before {
  1489. background: var(--ojb-overlay-background);
  1490. color: var(--ojb-color-text-secondary);
  1491. text-shadow: 0px 0px 2px #000000;
  1492. }
  1493. /* 阴影 */
  1494. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1495. box-shadow: var(--ojb-shadow-standard);
  1496. }
  1497. /* 图标按钮状态样式 */
  1498. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1499. color: var(--ojb-color-text-icon-success);
  1500. }
  1501. html[data-theme=dark] .ojb_btn_popover i:before {
  1502. text-shadow: var(--ojb-text-shadow-icon);
  1503. }
  1504. /* 其他样式 */
  1505. html[data-theme=dark] .OJBetter_setting_menu, html[data-theme=dark] .OJBetter_modal{
  1506. box-shadow: var(--ojb-shadow-menu-modal);
  1507. border: 1px solid var(--ojb-color-bg-secondary);
  1508. }
  1509. html[data-theme=dark] input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  1510. color: var(--ojb-color-text-primary);
  1511. border: 1px solid var(--ojb-color-border-radio-checked);
  1512. }
  1513. html[data-theme=dark] .alert{
  1514. text-shadow: none;
  1515. }
  1516. `);
  1517.  
  1518. // 网站界面样式
  1519. GM_addStyle(`
  1520. /* 文字颜色1 */
  1521. html[data-theme=dark] body, html[data-theme=dark] .title,
  1522. html[data-theme=dark] .problem-statement, html[data-theme=dark] #pageContent,
  1523. html[data-theme=dark] .ttypography, html[data-theme=dark] .roundbox, html[data-theme=dark] .info,
  1524. html[data-theme=dark] .ttypography .bordertable, html[data-theme=dark] .ttypography .bordertable thead th,
  1525. html[data-theme=dark] .ttypography h1, html[data-theme=dark] .ttypography h2, html[data-theme=dark] .ttypography h3,
  1526. html[data-theme=dark] .ttypography h4, html[data-theme=dark] .ttypography h5, html[data-theme=dark] .ttypography h6,
  1527. html[data-theme=dark] .datatable table, html[data-theme=dark] .problem-statement .sample-tests pre,
  1528. html[data-theme=dark] .ace-chrome .ace_gutter,
  1529. html[data-theme=dark] .setting-name,
  1530. html[data-theme=dark] .user-black, html[data-theme=dark] .comments label.show-archived,
  1531. html[data-theme=dark] .comments label.show-archived *, html[data-theme=dark] table{
  1532. color: var(--ojb-color-text-primary) !important;
  1533. }
  1534. html[data-theme=dark] h1 a, html[data-theme=dark] h2 a, html[data-theme=dark] h3 a, html[data-theme=dark] h4 a{
  1535. color: var(--ojb-color-text-secondary);
  1536. }
  1537. /* 文字颜色2 */
  1538. html[data-theme=dark] .contest-state-phase, html[data-theme=dark] .legendary-user-first-letter,
  1539. html[data-theme=dark] .lang-chooser,
  1540. html[data-theme=dark] .second-level-menu-list li a, html[data-theme=dark] #footer,
  1541. html[data-theme=dark] .ttypography .tt, html[data-theme=dark] select,
  1542. html[data-theme=dark] .roundbox .caption, html[data-theme=dark] .topic .title *,
  1543. html[data-theme=dark] .user-admin{
  1544. color: var(--ojb-color-text-secondary) !important;
  1545. }
  1546. /* 文字颜色3 */
  1547. html[data-theme=dark] #program-source-text-copy{
  1548. color: var(--ojb-color-text-secondary);
  1549. }
  1550. html[data-theme=dark] input{
  1551. color: var(--ojb-color-text-secondary) !important;
  1552. }
  1553. /* 文字颜色4 */
  1554. html[data-theme=dark] .ttypography .MathJax, html[data-theme=dark] .MathJax span{
  1555. color: var(--ojb-color-text-highlight) !important;
  1556. }
  1557. /* 链接颜色 */
  1558. html[data-theme=dark] a:link {
  1559. color: var(--ojb-color-text-link);
  1560. }
  1561. html[data-theme=dark] a:visited {
  1562. color: var(--ojb-color-text-secondary);
  1563. }
  1564. html[data-theme=dark] .menu-box a, html[data-theme=dark] .sidebox th a{
  1565. color: var(--ojb-color-text-secondary) !important;
  1566. }
  1567. /* 按钮 */
  1568. html[data-theme=dark] .second-level-menu-list li.backLava {
  1569. border-radius: 6px;
  1570. overflow: hidden;
  1571. filter: invert(1) hue-rotate(.5turn);
  1572. }
  1573. html[data-theme=dark] input:hover{
  1574. background-color: var(--ojb-color-bg-primary) !important;
  1575. }
  1576. /* 背景层次1 */
  1577. html[data-theme=dark] body, html[data-theme=dark] .ttypography .bordertable thead th,
  1578. html[data-theme=dark] .datatable table, html[data-theme=dark] .datatable .dark, html[data-theme=dark] li#add_button,
  1579. html[data-theme=dark] .problem-statement .sample-tests pre, html[data-theme=dark] .markItUpEditor,
  1580. html[data-theme=dark] .SumoSelect>.CaptionCont, html[data-theme=dark] .SumoSelect>.optWrapper,
  1581. html[data-theme=dark] .SumoSelect>.optWrapper.multiple>.options li.opt span i, html[data-theme=dark] .ace_scroller,
  1582. html[data-theme=dark] textarea, html[data-theme=dark] .state, html[data-theme=dark] .ace-chrome .ace_gutter-active-line,
  1583. html[data-theme=dark] .sidebar-menu ul li:hover, html[data-theme=dark] .sidebar-menu ul li.active,
  1584. html[data-theme=dark] .popup .content, html[data-theme=dark] .file.input-view .text, html[data-theme=dark] .file.output-view .text,
  1585. html[data-theme=dark] .file.answer-view .text, html[data-theme=dark] .file.checker-comment-view .text{
  1586. background-color: var(--ojb-color-bg-primary) !important;
  1587. background-image: none;
  1588. }
  1589. /* 背景层次2 */
  1590. html[data-theme=dark] .roundbox, html[data-theme=dark] .roundbox .dark, html[data-theme=dark] .bottom-links,
  1591. html[data-theme=dark] .spoiler-content, html[data-theme=dark] input,
  1592. html[data-theme=dark] .problem-statement .test-example-line-even, html[data-theme=dark] .highlight-blue,
  1593. html[data-theme=dark] .ttypography .tt, html[data-theme=dark] select,
  1594. html[data-theme=dark] .SumoSelect>.optWrapper>.options li.opt:hover,
  1595. html[data-theme=dark] .aceEditorTd, html[data-theme=dark] .ace-chrome .ace_gutter,
  1596. html[data-theme=dark] .datatable,
  1597. html[data-theme=dark] .highlighted-row td, html[data-theme=dark] .highlighted-row th,
  1598. html[data-theme=dark] .pagination span.active, html[data-theme=dark] .test-for-popup pre,
  1599. html[data-theme=dark] .source-popup pre{
  1600. background-color: var(--ojb-color-bg-secondary) !important;
  1601. }
  1602. /* 实线边框颜色-圆角 */
  1603. html[data-theme=dark] .roundbox, html[data-theme=dark] .roundbox .rtable td,
  1604. html[data-theme=dark] .sidebar-menu ul li,
  1605. html[data-theme=dark] input, html[data-theme=dark] .ttypography .tt, html[data-theme=dark] #items-per-page,
  1606. html[data-theme=dark] .datatable td, html[data-theme=dark] .datatable th,
  1607. html[data-theme=dark] textarea, html[data-theme=dark] .input-output-copier{
  1608. border: var(--ojb-border-solid-primary) !important;
  1609. border-radius: 2px;
  1610. }
  1611. /* 实线边框颜色-无圆角 */
  1612. html[data-theme=dark] .problem-statement .sample-tests .input,
  1613. html[data-theme=dark] .problem-statement .sample-tests .output, html[data-theme=dark] .pagination span.active,
  1614. html[data-theme=dark] .test-for-popup pre{
  1615. border: var(--ojb-border-solid-primary) !important;
  1616. }
  1617. html[data-theme=dark] .roundbox .titled, html[data-theme=dark] .roundbox .rtable th {
  1618. border-bottom: var(--ojb-border-solid-primary) !important;
  1619. }
  1620. html[data-theme=dark] .roundbox .bottom-links, html[data-theme=dark] #footer{
  1621. border-top: var(--ojb-border-solid-primary) !important;
  1622. }
  1623. html[data-theme=dark] .topic .content {
  1624. border-left: 4px solid var(--ojb-color-border-primary) !important;
  1625. }
  1626. html[data-theme=dark] hr {
  1627. border-color: var(--ojb-color-border-primary) !important;
  1628. }
  1629. /* 虚线边框 */
  1630. html[data-theme=dark] .comment-table{
  1631. border: var(--ojb-border-dashed);
  1632. }
  1633. /* input-output-copier特殊处理 */
  1634. html[data-theme=dark] .html2md-panel.input-output-copier,
  1635. html[data-theme=dark] .translateDiv.input-output-copier,
  1636. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier{
  1637. border: none !important;
  1638. }
  1639. html[data-theme=dark] .html2md-panel.input-output-copier:hover,
  1640. html[data-theme=dark] #OJBetter_SubmitForm.input-output-copier:hover{
  1641. background-color: #ffffff00 !important;
  1642. }
  1643. /* focus-visible */
  1644. html[data-theme=dark] input:focus-visible, html[data-theme=dark] textarea, html[data-theme=dark] select{
  1645. border-width: 1.5px !important;
  1646. outline: none;
  1647. }
  1648. /* 图片-亮度 */
  1649. html[data-theme=dark] img, html[data-theme=dark] #facebox .popup a{
  1650. opacity: .75;
  1651. }
  1652. /* 反转 */
  1653. html[data-theme=dark] .SumoSelect>.CaptionCont>label>i, html[data-theme=dark] .delete-resource-link,
  1654. html[data-theme=dark] #program-source-text, html[data-theme=dark] .spoiler-content pre,
  1655. html[data-theme=dark] .popup .content pre code{
  1656. filter: invert(1) hue-rotate(.5turn);
  1657. }
  1658. /* 阴影 */
  1659. html[data-theme=dark] .translate-problem-statement-panel, html[data-theme=dark] .translate-problem-statement{
  1660. box-shadow: var(--ojb-shadow-standard);
  1661. }
  1662. /* 图标按钮状态样式 */
  1663. html[data-theme=dark] .ojb_btn_popover.success i:before, html[data-theme=dark] .ojb_btn_popover.success i {
  1664. color: var(--ojb-color-text-icon-success);
  1665. }
  1666. html[data-theme=dark] .ojb_btn_popover i:before {
  1667. text-shadow: var(--ojb-text-shadow-icon);
  1668. }
  1669. /* 评测状态文字颜色 */
  1670. html[data-theme=dark] .verdict-accepted, html[data-theme=dark] .verdict-accepted-challenged,
  1671. html[data-theme=dark] .verdict-successful-challenge{
  1672. color: #0a0 !important;
  1673. }
  1674. html[data-theme=dark] .verdict-failed, html[data-theme=dark] .verdict-challenged{
  1675. color: red !important;
  1676. }
  1677. html[data-theme=dark] .verdict-rejected, html[data-theme=dark] .verdict-unsuccessful-challenge{
  1678. color: #673ab7 !important;
  1679. }
  1680. html[data-theme=dark] .verdict-waiting {
  1681. color: gray !important;
  1682. }
  1683. /* 样例hover样式 */
  1684. html[data-theme=dark] .problem-statement .darkhighlight {
  1685. background-color: #455a64 !important;
  1686. }
  1687. /* 其他样式 */
  1688. html[data-theme=dark] .rated-user{
  1689. display: initial;
  1690. }
  1691. html[data-theme=dark] .datatable .ilt, html[data-theme=dark] .datatable .irt,
  1692. html[data-theme=dark] .datatable .ilb, html[data-theme=dark] .datatable .irb,
  1693. html[data-theme=dark] .datatable .lt, html[data-theme=dark] .datatable .rt,
  1694. html[data-theme=dark] .datatable .lb, html[data-theme=dark] .datatable .rb{
  1695. background: none;
  1696. }
  1697. html[data-theme=dark] .problems .accepted-problem td.id{
  1698. border-left: 6px solid #47837d !important;
  1699. }
  1700. html[data-theme=dark] .problems .rejected-problem td.id{
  1701. border-left: 6px solid #ef9a9a !important;
  1702. }
  1703. html[data-theme=dark] .problems .accepted-problem td.act {
  1704. background-color: #47837d !important;
  1705. border-radius: 0px;
  1706. }
  1707. html[data-theme=dark] .problems .rejected-problem td.act{
  1708. background-color: #ef9a9a !important;
  1709. border-radius: 0px;
  1710. }
  1711. html[data-theme=dark] .collapsible-topic.collapsed .content .collapsible-topic-options:before{
  1712. background-image: linear-gradient(#22272e00, #22272e);
  1713. }
  1714. `);
  1715. })()
  1716.  
  1717. /**
  1718. * 黑暗模式额外的处理事件
  1719. */
  1720. function darkModeStyleAdjustment() {
  1721. $(".test-example-line").off("mouseenter mouseleave"); // 移除上面原本的事件
  1722. // 为奇数行添加悬停效果
  1723. $('.test-example-line-odd').hover(
  1724. function () {
  1725. $(this).addClass('darkhighlight');
  1726. $(this).prevUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1727. $(this).nextUntil(':not(.test-example-line-odd)').addClass('darkhighlight');
  1728. },
  1729. function () {
  1730. $(this).removeClass('darkhighlight');
  1731. $(this).prevUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1732. $(this).nextUntil(':not(.test-example-line-odd)').removeClass('darkhighlight');
  1733. }
  1734. );
  1735.  
  1736. // 为偶数行添加悬停效果
  1737. $('.test-example-line-even').hover(
  1738. function () {
  1739. $(this).addClass('darkhighlight');
  1740. $(this).prevUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1741. $(this).nextUntil(':not(.test-example-line-even)').addClass('darkhighlight');
  1742. },
  1743. function () {
  1744. $(this).removeClass('darkhighlight');
  1745. $(this).prevUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1746. $(this).nextUntil(':not(.test-example-line-even)').removeClass('darkhighlight');
  1747. }
  1748. );
  1749. }
  1750.  
  1751. /**
  1752. * 初始化monaco编辑器资源
  1753. */
  1754. async function initMonacoEditor() {
  1755. if (OJBetter.monaco.enableOnProblemPage || OJBetter.monaco.beautifyPreBlocks) {
  1756. try {
  1757. // 等待Monaco Editor加载器脚本加载完成
  1758. await OJB_LoadJS("https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs/loader.min.js");
  1759.  
  1760. // 配置Monaco Editor
  1761. require.config({
  1762. paths: { vs: "https://cdn.staticfile.net/monaco-editor/0.44.0/min/vs" },
  1763. "vs/nls": { availableLanguages: { "*": "zh-cn" } },
  1764. });
  1765.  
  1766. // 加载Monaco Editor主脚本
  1767. require(["vs/editor/editor.main"], () => {
  1768. OJBetter.monaco.loaderOnload = true;
  1769. });
  1770. } catch (error) {
  1771. console.error("Failed to load Monaco Editor: ", error);
  1772. }
  1773. }
  1774. }
  1775.  
  1776. /**
  1777. * 美化代码块
  1778. */
  1779. async function beautifyPreBlocksWithMonaco() {
  1780. // 判断monacoLoader是否加载完毕
  1781. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  1782.  
  1783. // 用于替换 <pre> 标签为 Monaco 编辑器的函数
  1784. function replacePreWithMonaco(preElement) {
  1785. const pre = $(preElement);
  1786. if (pre.hasClass('source-code-for-copy')) return; // 跳过复制块
  1787. const code = OJB_getCodeFromPre(pre.get(0));
  1788. if (!code) return;
  1789. const language = OJB_codeLangDetect(code);
  1790.  
  1791. // 创建一个用于 Monaco 编辑器的容器
  1792. const container = $('<div></div>');
  1793. const lineCount = code.split('\n').length; // 代码的行数
  1794.  
  1795. // 计算容器的高度
  1796. const calculateContainerHeight = (lineCount) => {
  1797. const lineHeight = 20; // 每行代码的高度
  1798. const minHeight = 100; // 最小高度
  1799. const maxHeight = 1000; // 最大高度
  1800. const dynamicHeight = lineCount * lineHeight;
  1801. return Math.min(Math.max(dynamicHeight, minHeight), maxHeight) + 'px';
  1802. };
  1803.  
  1804. // 应用样式
  1805. container.css({
  1806. height: calculateContainerHeight(lineCount),
  1807. width: '100%'
  1808. });
  1809. pre.hide();
  1810. pre.after(container);
  1811.  
  1812. // 初始化 Monaco 编辑器
  1813. const editor = monaco.editor.create(container[0], {
  1814. value: code,
  1815. language: language,
  1816. readOnly: true,
  1817. tabSize: 4,
  1818. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  1819. scrollbar: {
  1820. verticalScrollbarSize: 10,
  1821. horizontalScrollbarSize: 10,
  1822. alwaysConsumeMouseWheel: false
  1823. },
  1824. automaticLayout: true,
  1825. scrollBeyondLastLine: false
  1826. });
  1827.  
  1828. // 替换复制按钮事件
  1829. if (OJBetter.typeOfPage.is_submission) {
  1830. const button = $('div[title=Copy]');
  1831. console.log(button);
  1832. button.off('click');
  1833. button.on('click', (event) => {
  1834. event.stopPropagation(); // 阻止事件冒泡到 clipboard.min.js
  1835. const text = editor.getValue(); // 获取编辑器内的内容
  1836. GM_setClipboard(text);
  1837. Codeforces.showMessage("The source code has been copied into the clipboard");
  1838. });
  1839. }
  1840. }
  1841. // 全局替换页面上所有的 <pre> 元素
  1842. $('pre').each(function () {
  1843. replacePreWithMonaco(this);
  1844. });
  1845. // 监听页面上的提交状态页面窗口的 <pre> 元素
  1846. if (OJBetter.typeOfPage.is_statePage || OJBetter.typeOfPage.is_submissions) {
  1847. OJB_observeElement({
  1848. selector: '#facebox',
  1849. callback: (node) => {
  1850. // 如果 facebox 中存在 pre 元素,则替换它们
  1851. const preElements = $(node).find('pre');
  1852. preElements.each(function () {
  1853. replacePreWithMonaco(this);
  1854. });
  1855. }
  1856. });
  1857. }
  1858. }
  1859.  
  1860. /**
  1861. * 隐藏题目问题标签
  1862. */
  1863. function hiddenProblemTag() {
  1864. document.querySelectorAll('.roundbox.sidebox.borderTopRound .tag-box').forEach(element => {
  1865. if (!element.textContent.includes('*')) {
  1866. element.classList.add('hover-reveal');
  1867. }
  1868. });
  1869. }
  1870.  
  1871. // 样式
  1872. GM_addStyle(`
  1873. /*动画*/
  1874. @keyframes shake {
  1875. 0% { transform: translateX(-5px); }
  1876. 100% { transform: translateX(5px); }
  1877. }
  1878. @keyframes rotate {
  1879. from {
  1880. transform: rotate(0deg);
  1881. }
  1882.  
  1883. to {
  1884. transform: rotate(360deg);
  1885. }
  1886. }
  1887. @keyframes rippleout {
  1888. 0% {
  1889. box-shadow: 0 0 0 0 rgba(96, 98, 102, 0.2);
  1890. }
  1891.  
  1892. 100% {
  1893. box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
  1894. }
  1895. }
  1896. @keyframes bounce-in {
  1897. 20%,40%,60%,80%,from,to {
  1898. animation-timing-function: cubic-bezier(.215,.61,.355,1);
  1899. }
  1900.  
  1901. 0% {
  1902. opacity: 0;
  1903. transform: scale3d(.995,.995,.995);
  1904. }
  1905.  
  1906. 20% {
  1907. opacity: 1;
  1908. transform: scale3d(1.005,1.005,1.005);
  1909. }
  1910.  
  1911. 40% {
  1912. transform: scale3d(.998,.998,.998);
  1913. }
  1914.  
  1915. 60% {
  1916. transform: scale3d(1.002,1.002,1.002);
  1917. }
  1918.  
  1919. 80% {
  1920. transform: scale3d(.995,.995,.995);
  1921. }
  1922.  
  1923. to {
  1924. opacity: 1;
  1925. transform: scale3d(1,1,1);
  1926. }
  1927. }
  1928. /*iconfont图标*/
  1929. .iconfont {
  1930. font-family: "iconfont" !important;
  1931. font-size: 16px;
  1932. font-style: normal !important;
  1933. -webkit-font-smoothing: antialiased;
  1934. -moz-osx-font-smoothing: grayscale;
  1935. }
  1936. @font-face {
  1937. font-family: 'iconfont'; /* Project id 4284341 */
  1938. src: url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2') format('woff2'),
  1939. url('//aowuucdn.oss-accelerate.aliyuncs.com/iconfont/iconfont.woff2.ttf') format('truetype');
  1940. }
  1941. html {
  1942. scroll-behavior: smooth;
  1943. }
  1944. :root {
  1945. --vp-font-family-base: "Chinese Quotes", "Inter var", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  1946. }
  1947. span.mdViewContent {
  1948. white-space: pre-wrap;
  1949. }
  1950.  
  1951. /* 特殊处理,加上input-output-copier类, 让convertStatementToText方法忽略该元素 */
  1952. .translateDiv.input-output-copier, .html2md-panel.input-output-copier, #OJBetter_SubmitForm.input-output-copier {
  1953. font-size: initial;
  1954. float: initial;
  1955. color: initial;
  1956. cursor: initial;
  1957. border: none;
  1958. padding: 0px;
  1959. margin: 0px;
  1960. line-height: initial;
  1961. text-transform: none;
  1962. }
  1963. .translateDiv.input-output-copier:hover, .html2md-panel.input-output-copier:hover, #OJBetter_SubmitForm.input-output-copier:hover {
  1964. background-color: #ffffff00;
  1965. }
  1966.  
  1967. /* dialog */
  1968. dialog {
  1969. margin: 0px !important;
  1970. }
  1971. dialog::backdrop {
  1972. background-color: rgba(0, 0, 0, 0.4);
  1973. }
  1974.  
  1975. /*题目页链接栏样式*/
  1976. #problemToolbar {
  1977. display: flex;
  1978. flex-wrap: wrap;
  1979. justify-content: flex-end;
  1980. overflow: auto;
  1981. height: 100%;
  1982. margin: 0.5em;
  1983. }
  1984.  
  1985. /*html2md面板*/
  1986. .html2md-panel {
  1987. display: flex;
  1988. justify-content: flex-end;
  1989. align-items: center;
  1990. }
  1991. .html2md-panel a {
  1992. text-decoration: none;
  1993. }
  1994. .html2md-panel > button {
  1995. margin: 5px;
  1996. }
  1997. .html2md-panel.is_simple {
  1998. position: absolute;
  1999. right: 2%;
  2000. }
  2001.  
  2002. /*通用按钮*/
  2003. .ojb_btn {
  2004. display: flex;
  2005. align-items: center;
  2006. justify-content: center;
  2007. cursor: pointer;
  2008. background-color: #ffffff;
  2009. color: #606266;
  2010. width: auto;
  2011. font-size: 13px;
  2012. border-radius: 0.3rem;
  2013. padding: 2px 5px;
  2014. margin: 0px 5px;
  2015. border: 1px solid #dcdfe6;
  2016. }
  2017. .ojb_btn[disabled] {
  2018. cursor: not-allowed !important;
  2019. color: rgb(168, 171, 178) !important;
  2020. border: 1px solid #e4e7ed;
  2021. background-color: #ffffff;
  2022. }
  2023. .ojb_btn:hover {
  2024. color: #409eff;
  2025. border-color: #409eff;
  2026. background-color: #f1f8ff;
  2027. z-index: 150;
  2028. }
  2029. .ojb_btn.primary {
  2030. color: #ffffff;
  2031. border: 1px solid #409eff;
  2032. background-color: #409eff;
  2033. }
  2034. .ojb_btn.primary:hover {
  2035. color: #ffffff;
  2036. border: 1px solid #79bbff;
  2037. background-color: #79bbff;
  2038. }
  2039. .ojb_btn.success {
  2040. color: #4caf50;
  2041. border: 1px solid #C8E6C9;
  2042. background-color: #f0f9eb;
  2043. }
  2044. .ojb_btn.warning {
  2045. color: #e6a23c;
  2046. border: 1px solid #f3d19e;
  2047. background-color: #fdf6ec;
  2048. }
  2049. .ojb_btn.error {
  2050. color: #f56c6c;
  2051. border: 1px solid #fab6b6;
  2052. background-color: #fef0f0;
  2053. }
  2054. .ojb_btn.enabled {
  2055. color: #42A5F5;
  2056. border: 1px solid #90CAF9;
  2057. background-color: #fafbff;
  2058. }
  2059. .ojb_btn.active {
  2060. animation: rippleout 0.5s ease-in-out;
  2061. }
  2062. a.ojb_btn {
  2063. text-decoration: none;
  2064. }
  2065. a.ojb_btn:link {
  2066. color: #606266;
  2067. }
  2068. a.ojb_btn span {
  2069. margin-left: 2px;
  2070. }
  2071. /*按钮图标和popover*/
  2072. .ojb_btn_popover {
  2073. display: flex;
  2074. justify-content: center;
  2075. position: relative;
  2076. outline: none;
  2077. appearance: none;
  2078. }
  2079. .ojb_btn_popover:hover span {
  2080. opacity: 1;
  2081. visibility: visible;
  2082. }
  2083. .ojb_btn_popover i:before {
  2084. position: absolute;
  2085. text-shadow: 1px 1px 0px #ffffff, 1px -1px 0px #ffffff, -1px -1px 0px #ffffff, -1px 1px 0px #ffffff;
  2086. }
  2087. .ojb_btn_popover span {
  2088. cursor: initial;
  2089. position: absolute;
  2090. left: 50%;
  2091. opacity: 0;
  2092. visibility: hidden;
  2093. padding: 4px 8px;
  2094. background-color: rgba(33, 33, 33, 0.8);
  2095. color: rgba(255, 255, 255, 0.9019607843);
  2096. font-size: 12px;
  2097. border-radius: 6px;
  2098. line-height: 1.6;
  2099. text-align: left;
  2100. white-space: nowrap;
  2101. transition: all 0.15s ease-in-out;
  2102. z-index: 999;
  2103. }
  2104. .ojb_btn_popover span:hover {
  2105. opacity: 0;
  2106. visibility: hidden;
  2107. }
  2108. .ojb_btn_popover.top:hover span {
  2109. transform: translate(-50%, 0);
  2110. }
  2111. .ojb_btn_popover.top span {
  2112. bottom: 100%;
  2113. transform: translate(-50%, -20%);
  2114. margin-bottom: 4px;
  2115. }
  2116. .ojb_btn_popover.top span:hover {
  2117. transform: translate(-50%, -20%);
  2118. }
  2119. .ojb_btn_popover.bottom:hover span {
  2120. transform: translate(-50%, 105%);
  2121. }
  2122. .ojb_btn_popover.bottom span {
  2123. bottom: -2%;
  2124. transform: translate(-50%, 100%);
  2125. margin-top: 4px;
  2126. }
  2127. .ojb_btn_popover.bottom span:hover {
  2128. transform: translate(-50%, 50%);
  2129. }
  2130. .ojb_btn_popover.loading i {
  2131. color: rgba(33, 33, 33, 0.1);
  2132. }
  2133. .ojb_btn_popover.loading i:before {
  2134. content: "\\e640";
  2135. color: rgb(168, 171, 178);
  2136. animation: rotate 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
  2137. }
  2138. .ojb_btn_popover.running i {
  2139. color: rgba(33, 33, 33, 0.1);
  2140. }
  2141. .ojb_btn_popover.running i:before {
  2142. content: "\\e600";
  2143. color: rgb(168, 171, 178);
  2144. animation: rotate 1s linear infinite;
  2145. }
  2146. .ojb_btn_popover.warning i {
  2147. color: rgba(230, 162, 61, 0.8);
  2148. }
  2149. .ojb_btn_popover.warning i:before {
  2150. content: "\\e68b";
  2151. font-size: 15px;
  2152. left: 10px;
  2153. bottom: 0%;
  2154. color: #ff9800;
  2155. }
  2156. .ojb_btn_popover.error i {
  2157. color: rgba(245, 108, 108, 0.8);
  2158. }
  2159. .ojb_btn_popover.error i:before {
  2160. content: "\\e651";
  2161. font-size: 15px;
  2162. left: 10px;
  2163. bottom: 0%;
  2164. color: #F44336;
  2165. }
  2166. .ojb_btn_popover.success i {
  2167. color: rgba(76, 175, 80, 0.9);
  2168. }
  2169. .ojb_btn_popover.success i:before {
  2170. content: "\\e61e";
  2171. font-size: 15px;
  2172. left: 10px;
  2173. bottom: 0%;
  2174. color: #4caf50;
  2175. }
  2176. .ojb_btn_popover.enabled i {
  2177. color: rgba(33, 150, 243, 0.6);
  2178. }
  2179. .ojb_btn_popover.enabled i:before {
  2180. content: "\\e6f4";
  2181. font-size: 15px;
  2182. left: 10px;
  2183. bottom: 0%;
  2184. color: #2196F3;
  2185. }
  2186. .ojb_btn_popover.redo i {
  2187. color: rgba(33, 33, 33, 0.1);
  2188. }
  2189. .ojb_btn_popover.redo i:before {
  2190. content: "\\e831";
  2191. color: #616161;
  2192. }
  2193. .ojb_btn_popover.reverse i {
  2194. transform: rotate(180deg);
  2195. }
  2196.  
  2197. /*translateDiv样式*/
  2198. .translateDiv .topText {
  2199. display: flex;
  2200. margin-left: 5px;
  2201. color: #9e9e9e;
  2202. font-size: 13px;
  2203. align-items: center;
  2204. }
  2205. .translateDiv .borderlessButton{
  2206. display: flex;
  2207. align-items: center;
  2208. margin: 2.5px 7px;
  2209. fill: #9E9E9E;
  2210. }
  2211. .translateDiv .borderlessButton:hover{
  2212. cursor: pointer;
  2213. fill: #059669;
  2214. }
  2215. .translateDiv.bounce-in {
  2216. animation: bounce-in 1s forwards;
  2217. }
  2218. html:not([data-theme='dark']) .translateDiv {
  2219. box-shadow: 0px 0px 0.5px 0.5px #defdf378;
  2220. }
  2221. .translate-problem-statement {
  2222. justify-items: start;
  2223. letter-spacing: 1.8px;
  2224. color: #059669;
  2225. background-color: #f9f9fa;
  2226. border: 1px solid #c5ebdf;
  2227. border-radius: 0rem 0rem 0.3rem 0.3rem;
  2228. padding: 5px;
  2229. margin: -5px 0px 6px 0px;
  2230. width: 100%;
  2231. box-sizing: border-box;
  2232. font-size: 13px;
  2233. }
  2234. .translate-problem-statement h2 {
  2235. font-size: 1.6em;
  2236. font-weight: 700;
  2237. }
  2238. .translate-problem-statement h3 {
  2239. font-size: 1.3em;
  2240. font-weight: 700;
  2241. }
  2242. .translate-problem-statement-panel{
  2243. display: flex;
  2244. justify-content: space-between;
  2245. background-color: #f9f9fa;
  2246. border: 1px solid #c5ebdf;
  2247. border-radius: 0.3rem;
  2248. margin: 4px 0px;
  2249. }
  2250. .translate-problem-statement-panel .ojb_btn {
  2251. background: none;
  2252. border: none;
  2253. color: #9e9e9e;
  2254. }
  2255. .translate-problem-statement-panel.error, .translate-problem-statement.error {
  2256. color: red;
  2257. border-color: red;
  2258. }
  2259. .translate-problem-statement a, .translate-problem-statement a:link {
  2260. color: #10b981;
  2261. font-weight: 600;
  2262. background: 0 0;
  2263. text-decoration: none;
  2264. }
  2265. .translate-problem-statement ol, .translate-problem-statement ul {
  2266. display: grid;
  2267. margin-inline-start: 0.8em;
  2268. margin-block-start: 0em;
  2269. margin: 0.5em 0 0 3em;
  2270. padding-inline-start: 0px;
  2271. }
  2272. .translate-problem-statement li {
  2273. display: list-item;
  2274. height: auto;
  2275. word-wrap: break-word;
  2276. }
  2277. .translate-problem-statement ol li {
  2278. list-style-type: auto;
  2279. }
  2280. .translate-problem-statement ul li {
  2281. list-style-type: disc;
  2282. }
  2283. .translate-problem-statement img {
  2284. max-width: 100.0%;
  2285. max-height: 100.0%;
  2286. }
  2287. .ttypography .translate-problem-statement .MathJax {
  2288. color: #059669!important;
  2289. }
  2290. .translate-problem-statement span.math {
  2291. margin: 0px 2.5px !important;
  2292. }
  2293. .translate-problem-statement a:hover {
  2294. background-color: #800;
  2295. color: #fff;
  2296. text-decoration: none;
  2297. }
  2298. .translate-problem-statement table {
  2299. border: 1px #ccc solid !important;
  2300. margin: 1.5em 0 !important;
  2301. color: #059669 !important;
  2302. }
  2303. .translate-problem-statement table thead th {
  2304. border: 1px #ccc solid !important;
  2305. color: #059669 !important;
  2306. }
  2307. .translate-problem-statement table td {
  2308. border-right: 1px solid #ccc;
  2309. border-top: 1px solid #ccc;
  2310. padding: 0.7143em 0.5em;
  2311. }
  2312. .translate-problem-statement table th {
  2313. padding: 0.7143em 0.5em;
  2314. }
  2315. .translate-problem-statement p:not(:first-child) {
  2316. margin: 1.5em 0 0;
  2317. }
  2318. .translate-problem-statement p {
  2319. line-height: 20px !important;
  2320. word-wrap: break-word;
  2321. font-size: 13px !important
  2322. }
  2323. .problem-statement p:last-child {
  2324. margin-bottom: 0px !important;
  2325. }
  2326.  
  2327. /*设置按钮*/
  2328. header .enter-or-register-box, header .languages {
  2329. position: absolute;
  2330. right: 170px;
  2331. }
  2332. .ojb_btn.OJBetter_setting {
  2333. float: right;
  2334. height: 30px;
  2335. background: #60a5fa;
  2336. color: white;
  2337. margin: 10px;
  2338. border: 1px solid #60a5fa;
  2339. }
  2340. .ojb_btn.OJBetter_setting.open {
  2341. background-color: #e6e6e6;
  2342. color: #727378;
  2343. cursor: not-allowed;
  2344. }
  2345.  
  2346. /*设置面板*/
  2347. .OJBetter_setting_menu {
  2348. box-shadow: 0px 0px 0px 4px #ffffff;
  2349. position: fixed;
  2350. top: 50%;
  2351. left: 50%;
  2352. width: 600px;
  2353. min-height: 600px;
  2354. transform: translate(-50%, -50%);
  2355. border-radius: 6px;
  2356. background-color: #f0f4f9;
  2357. border-collapse: collapse;
  2358. border: 1px solid #ffffff;
  2359. color: #697e91;
  2360. font-family: var(--vp-font-family-base);
  2361. padding: 10px 20px 20px 10px;
  2362. box-sizing: content-box;
  2363. }
  2364. .OJBetter_setting_menu h3 {
  2365. margin-top: 10px;
  2366. font-size: 1.4em;
  2367. font-weight: 700;
  2368. }
  2369. .OJBetter_setting_menu h4 {
  2370. margin: 15px 0px 10px 0px;
  2371. }
  2372. .OJBetter_setting_menu h4,.OJBetter_setting_menu h5 {
  2373. font-weight: 600;
  2374. }
  2375. .OJBetter_setting_menu hr {
  2376. border: none;
  2377. height: 1px;
  2378. background-color: #ccc;
  2379. margin: 10px 0;
  2380. }
  2381. .OJBetter_setting_menu details {
  2382. padding: 10px;
  2383. margin-bottom: 5px;
  2384. background-color: #ffffff;
  2385. border-bottom: 1px solid #c9c6c696;
  2386. border-radius: 8px;
  2387. }
  2388. .OJBetter_setting_menu .badge {
  2389. border-radius: 4px;
  2390. border: 1px solid #009688;
  2391. color: #009688;
  2392. background-color: #fff;
  2393. padding: 0.5px 4px;
  2394. margin-left: 5px;
  2395. margin-right: auto;
  2396. line-height: initial;
  2397. font-weight: initial;
  2398. }
  2399. .OJBetter_setting_menu .missing {
  2400. box-shadow: inset 0px 0px 1px 1px red;
  2401. }
  2402. /* 页面切换 */
  2403. .OJBetter_setting_menu .settings-page {
  2404. display: none;
  2405. }
  2406. .OJBetter_setting_menu .settings-page.active {
  2407. display: block;
  2408. }
  2409. .OJBetter_setting_container {
  2410. display: flex;
  2411. }
  2412. .OJBetter_setting_sidebar {
  2413. flex: 0 0 auto;
  2414. min-width: 110px;
  2415. padding: 6px 10px 6px 6px;
  2416. margin: 20px 0px;
  2417. border-right: 1px solid #d4d8e9;
  2418. }
  2419. .OJBetter_setting_content {
  2420. flex-grow: 1;
  2421. margin: 20px 0px 0px 12px;
  2422. padding-right: 10px;
  2423. max-height: 580px;
  2424. overflow-y: auto;
  2425. box-sizing: border-box;
  2426. }
  2427. .OJBetter_setting_sidebar h3 {
  2428. margin-top: 0;
  2429. }
  2430. .OJBetter_setting_sidebar hr {
  2431. margin-top: 10px;
  2432. margin-bottom: 10px;
  2433. border: none;
  2434. border-top: 1px solid #DADCE0;
  2435. }
  2436. .OJBetter_setting_sidebar ul {
  2437. list-style-type: none;
  2438. margin: 0;
  2439. padding: 0;
  2440. }
  2441. .OJBetter_setting_sidebar li {
  2442. margin: 5px 0px;
  2443. background-color: #ffffff;
  2444. border: 1px solid #d4d8e9;
  2445. border-radius: 4px;
  2446. font-size: 16px;
  2447. }
  2448. .OJBetter_setting_sidebar li a {
  2449. text-decoration: none;
  2450. display: flex;
  2451. width: 100%;
  2452. font-size: 16px;
  2453. color: gray;
  2454. background-color: #ffffff;
  2455. border: none;
  2456. letter-spacing: 2px;
  2457. padding: 7px;
  2458. margin: 0px;
  2459. border-radius: 4px;
  2460. align-items: center;
  2461. -webkit-box-sizing: border-box;
  2462. -moz-box-sizing: border-box;
  2463. box-sizing: border-box;
  2464. }
  2465. .OJBetter_setting_sidebar li a.active {
  2466. background-color: #eceff1c7;
  2467. }
  2468. /* 链接样式 */
  2469. .OJBetter_setting_menu a {
  2470. font-size: 13px;
  2471. color: #009688;
  2472. background-color: #E0F2F1;
  2473. border: 1px solid #009688;
  2474. border-radius: 4px;
  2475. padding: 0px 5px;
  2476. margin: 0px 5px;
  2477. text-decoration: none;
  2478. }
  2479. /* 下拉选择框 */
  2480. .OJBetter_setting_menu select {
  2481. appearance: none;
  2482. padding: 5px 10px;
  2483. margin: -5px 0px;
  2484. border-radius: 6px;
  2485. border-style: solid;
  2486. border: 1px solid #ced4da;
  2487. color: #009688;
  2488. background: #ffffff;
  2489. font-size: 15px;
  2490. }
  2491. .OJBetter_setting_menu select:focus-visible {
  2492. outline: none;
  2493. }
  2494. .OJBetter_setting_menu select option:disabled {
  2495. color: #EEEEEE;
  2496. }
  2497. /* 数值输入框 */
  2498. .OJBetter_setting_menu input[type="number"] {
  2499. width: 40px;
  2500. color: #009688;
  2501. font-size: 15px;
  2502. appearance: none;
  2503. padding: 5px 10px;
  2504. margin: -5px 3px;
  2505. border-radius: 6px;
  2506. border-style: solid;
  2507. border: 1px solid #ced4da;
  2508. }
  2509. .OJBetter_setting_menu input[type="number"]:focus-visible {
  2510. outline: none;
  2511. }
  2512. .OJBetter_setting_menu input[type="number"]::-webkit-inner-spin-button,
  2513. .OJBetter_setting_menu input[type="number"]::-webkit-outer-spin-button {
  2514. -webkit-appearance: none;
  2515. margin: 0;
  2516. }
  2517. /*设置面板-滚动条*/
  2518. .OJBetter_setting_menu::-webkit-scrollbar, .OJBetter_setting_content::-webkit-scrollbar,
  2519. .OJBetter_modal .content::-webkit-scrollbar {
  2520. width: 5px;
  2521. height: 7px;
  2522. background-color: #aaa;
  2523. }
  2524. .OJBetter_setting_menu::-webkit-scrollbar-thumb, .OJBetter_setting_content::-webkit-scrollbar-thumb,
  2525. .OJBetter_modal .content::-webkit-scrollbar-thumb {
  2526. background-clip: padding-box;
  2527. background-color: #d7d9e4;
  2528. }
  2529. .OJBetter_setting_menu::-webkit-scrollbar-track, .OJBetter_setting_content::-webkit-scrollbar-track,
  2530. .OJBetter_modal .content::-webkit-scrollbar-track {
  2531. background-color: #f1f1f1;
  2532. }
  2533. /*设置面板-关闭按钮*/
  2534. .OJBetter_setting_menu .tool-box {
  2535. position: absolute;
  2536. width: 20px;
  2537. height: 20px;
  2538. top: 3px;
  2539. right: 3px;
  2540. }
  2541. .OJBetter_setting_menu .btn-close {
  2542. width: 20px;
  2543. height: 20px;
  2544. border-radius: 50%;
  2545. border: none;
  2546. margin: 0px;
  2547. padding: 0px;
  2548. background-color: #ff000080;
  2549. transition: .15s ease all;
  2550. box-sizing: border-box;
  2551. text-align: center;
  2552. color: transparent;
  2553. }
  2554. .OJBetter_setting_menu .iconfont {
  2555. font-size: 10px;
  2556. font-weight: bolder;
  2557. }
  2558. .OJBetter_setting_menu .btn-close:hover {
  2559. color: #ffffff;
  2560. background-color: #ff0000cc;
  2561. box-shadow: 0 5px 5px 0 #00000026;
  2562. }
  2563. .OJBetter_setting_menu .btn-close:active {
  2564. color: #ffffffde;
  2565. background-color: #ff000080;
  2566. }
  2567. /*设置面板-checkbox*/
  2568. .OJBetter_setting_menu input[type=checkbox]:focus {
  2569. outline: 0px;
  2570. }
  2571. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"] {
  2572. margin: 0px;
  2573. appearance: none;
  2574. -webkit-appearance: none;
  2575. width: 40px;
  2576. height: 20px;
  2577. border: 1.5px solid #D7CCC8;
  2578. padding: 0px !important;
  2579. border-radius: 20px;
  2580. background: #efebe978;
  2581. position: relative;
  2582. box-sizing: border-box;
  2583. }
  2584. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::before {
  2585. content: "";
  2586. width: 17px;
  2587. height: 17px;
  2588. background: #D7CCC8;
  2589. border: 1.5px solid #BCAAA4;
  2590. border-radius: 50%;
  2591. position: absolute;
  2592. top: 0;
  2593. left: 0;
  2594. transform: translate(2%, 2%);
  2595. transition: all 0.3s ease-in-out;
  2596. box-sizing: border-box;
  2597. }
  2598. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]::after {
  2599. content: url("data:image/svg+xml,%3Csvg xmlns='://www.w3.org/2000/svg' width='23' height='23' viewBox='0 0 23 23' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.55021 5.84315L17.1568 16.4498L16.4497 17.1569L5.84311 6.55026L6.55021 5.84315Z' fill='%23EA0707' fill-opacity='0.89'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M17.1567 6.55021L6.55012 17.1568L5.84302 16.4497L16.4496 5.84311L17.1567 6.55021Z' fill='%23EA0707' fill-opacity='0.89'/%3E%3C/svg%3E");
  2600. position: absolute;
  2601. top: 0;
  2602. left: 24px;
  2603. }
  2604. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked {
  2605. border: 1.5px solid #C5CAE9;
  2606. background: #E8EAF6;
  2607. }
  2608. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::before {
  2609. background: #C5CAE9;
  2610. border: 1.5px solid #7986CB;
  2611. transform: translate(122%, 2%);
  2612. transition: all 0.3s ease-in-out;
  2613. }
  2614. .OJBetter_setting_menu .OJBetter_setting_list input[type="checkbox"]:checked::after {
  2615. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 15 13' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M14.8185 0.114533C15.0314 0.290403 15.0614 0.605559 14.8855 0.818454L5.00187 12.5L0.113036 6.81663C-0.0618274 6.60291 -0.0303263 6.2879 0.183396 6.11304C0.397119 5.93817 0.71213 5.96967 0.886994 6.18339L5.00187 11L14.1145 0.181573C14.2904 -0.0313222 14.6056 -0.0613371 14.8185 0.114533Z' fill='%2303A9F4' fill-opacity='0.9'/%3E%3C/svg%3E");
  2616. position: absolute;
  2617. top: 1.5px;
  2618. left: 4.5px;
  2619. }
  2620. .OJBetter_setting_menu .OJBetter_setting_list button {
  2621. cursor: pointer;
  2622. color: #7986cb;
  2623. background-color: #e8eaf6;
  2624. border: 1px solid #7986cb;
  2625. border-radius: 6px;
  2626. width: 100px;
  2627. margin: -5px 2px;
  2628. padding: 5px 10px;
  2629. }
  2630. .OJBetter_setting_menu .OJBetter_setting_list button:hover {
  2631. color: #e8eaf6;
  2632. background-color: #7986cb;
  2633. border: 1px solid #7986cb;
  2634. }
  2635. .OJBetter_setting_menu label, #darkMode_span, #loaded_span {
  2636. font-size: 16px;
  2637. }
  2638. .OJBetter_setting_list {
  2639. display: flex;
  2640. flex-wrap: wrap;
  2641. align-items: center;
  2642. padding: 10px;
  2643. margin: 5px 0px;
  2644. background-color: #ffffff;
  2645. border: 1px solid #c9c6c642;
  2646. border-bottom-color: #c9c6c696;
  2647. border-radius: 8px;
  2648. justify-content: space-between;
  2649. }
  2650. .OJBetter_setting_list.alert_danger {
  2651. color: #F44336;
  2652. background-color: #FFEBEE;
  2653. border: 1px solid #F44336;
  2654. margin: 10px 0px;
  2655. }
  2656. .OJBetter_setting_list.alert_warn {
  2657. color: #E65100;
  2658. background-color: #FFF3E0;
  2659. border: 1px solid #FF9800;
  2660. margin: 10px 0px;
  2661. }
  2662. .OJBetter_setting_list.alert_tip {
  2663. color: #009688;
  2664. background-color: #E0F2F1;
  2665. border: 1px solid #009688;
  2666. margin: 10px 0px;
  2667. }
  2668. .OJBetter_setting_list.alert_info {
  2669. color: #ffffff;
  2670. background-color: #009688;
  2671. margin: 10px 0px;
  2672. box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset;
  2673. }
  2674. .OJBetter_setting_list p:not(:last-child) {
  2675. margin-bottom: 10px;
  2676. }
  2677. .OJBetter_setting_list p:not(:first-child) {
  2678. margin-top: 10px;
  2679. }
  2680. /*设置面板-checkboxs*/
  2681. .OJBetter_setting_menu .OJBetter_checkboxs {
  2682. flex-basis: 100%;
  2683. display: flex;
  2684. padding: 8px;
  2685. margin: 10px 0px 0px 0px;
  2686. border-bottom: 1px solid #c9c6c696;
  2687. border-radius: 8px;
  2688. border: 1px solid #c5cae9;
  2689. background-color: #f0f8ff;
  2690. }
  2691. .OJBetter_setting_menu .OJBetter_checkboxs label {
  2692. font-size: 13px;
  2693. margin: 0px 6px 0px 3px;
  2694. }
  2695. .OJBetter_setting_menu .OJBetter_checkboxs input[type=checkbox]:checked+label{
  2696. color: #7986cb;
  2697. }
  2698. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"] {
  2699. border: none;
  2700. width: 16px;
  2701. height: 16px;
  2702. }
  2703. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]::before{
  2704. background: #ffffff;
  2705. transform: none;
  2706. width: 16px;
  2707. height: 16px;
  2708. }
  2709. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked {
  2710. background: none;
  2711. border: none;
  2712. }
  2713. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::before {
  2714. border: 1.5px solid #95a2de;
  2715. background: #e8eaf6;
  2716. transform: none;
  2717. }
  2718. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:checked::after {
  2719. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' viewBox='0 0 15 13' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M14.8185 0.114533C15.0314 0.290403 15.0614 0.605559 14.8855 0.818454L5.00187 12.5L0.113036 6.81663C-0.0618274 6.60291 -0.0303263 6.2879 0.183396 6.11304C0.397119 5.93817 0.71213 5.96967 0.886994 6.18339L5.00187 11L14.1145 0.181573C14.2904 -0.0313222 14.6056 -0.0613371 14.8185 0.114533Z' fill='%2303A9F4' fill-opacity='0.9'/%3E%3C/svg%3E");
  2720. top: 0px;
  2721. left: 3.5px;
  2722. }
  2723. .OJBetter_setting_menu .OJBetter_checkboxs input[type="checkbox"]:disabled+label {
  2724. color: #BDBDBD;
  2725. }
  2726. /*设置面板-radio*/
  2727. .OJBetter_setting_menu label {
  2728. display: block;
  2729. font-weight: initial;
  2730. list-style-type: none;
  2731. padding-inline-start: 0px;
  2732. overflow-x: auto;
  2733. max-width: 100%;
  2734. margin: 3px 0px;
  2735. overflow-x: visible;
  2736. }
  2737. .OJBetter_setting_menu_label_text {
  2738. display: flex;
  2739. border: 1px dashed #00aeeccc;
  2740. height: 35px;
  2741. width: 100%;
  2742. color: #6e6e6e;
  2743. font-weight: 300;
  2744. font-size: 14px;
  2745. letter-spacing: 2px;
  2746. padding: 7px;
  2747. margin-bottom: 4px;
  2748. align-items: center;
  2749. -webkit-box-sizing: border-box;
  2750. -moz-box-sizing: border-box;
  2751. box-sizing: border-box;
  2752. }
  2753. input[type="radio"]:checked+.OJBetter_setting_menu_label_text {
  2754. background: #41e49930;
  2755. border: 1px solid green;
  2756. color: green;
  2757. text-shadow: 0px 0px 0.5px green;
  2758. }
  2759. input[type="radio"]:disabled+.OJBetter_setting_menu_label_text {
  2760. background: #fafafa00;
  2761. border: 1px solid #e0e0e07a;
  2762. color: #e0e0e0;
  2763. }
  2764. .OJBetter_setting_menu label input[type="radio"], .OJBetter_contextmenu label input[type="radio"]{
  2765. appearance: none;
  2766. list-style: none;
  2767. padding: 0px !important;
  2768. margin: 0px;
  2769. clip: rect(0 0 0 0);
  2770. -webkit-clip-path: inset(100%);
  2771. clip-path: inset(100%);
  2772. height: 1px;
  2773. overflow: hidden;
  2774. position: absolute;
  2775. white-space: nowrap;
  2776. width: 1px;
  2777. }
  2778. /*设置面板-文本输入框*/
  2779. .OJBetter_setting_menu input[type="text"] {
  2780. display: block;
  2781. height: 25px !important;
  2782. width: 100%;
  2783. background-color: #ffffff;
  2784. color: #727378;
  2785. font-size: 12px;
  2786. border-radius: 0.3rem;
  2787. padding: 1px 5px !important;
  2788. box-sizing: border-box;
  2789. margin: 5px 0px 5px 0px;
  2790. border: 1px solid #00aeeccc;
  2791. box-shadow: 0 0 1px #0000004d;
  2792. }
  2793. .OJBetter_setting_menu .OJBetter_setting_list input[type="text"] {
  2794. margin-left: 5px;
  2795. }
  2796. .OJBetter_setting_menu input[type="text"]:focus-visible{
  2797. border-style: solid;
  2798. border-color: #3f51b5;
  2799. outline: none;
  2800. }
  2801. .OJBetter_setting_menu_config_box {
  2802. width: 100%;
  2803. display: grid;
  2804. margin-top: 5px;
  2805. -webkit-box-sizing: border-box;
  2806. -moz-box-sizing: border-box;
  2807. box-sizing: border-box;
  2808. }
  2809. .OJBetter_setting_menu input::placeholder {
  2810. color: #727378;
  2811. }
  2812. .OJBetter_setting_menu input.no_default::placeholder{
  2813. color: #BDBDBD;
  2814. }
  2815. .OJBetter_setting_menu input.is_null::placeholder{
  2816. color: red;
  2817. border-width: 1.5px;
  2818. }
  2819. .OJBetter_setting_menu input.is_null{
  2820. border-color: red;
  2821. }
  2822. .OJBetter_setting_menu textarea {
  2823. resize: vertical;
  2824. display: block;
  2825. width: 100%;
  2826. height: 60px;
  2827. background-color: #ffffff;
  2828. color: #727378;
  2829. font-size: 12px;
  2830. padding: 1px 5px !important;
  2831. box-sizing: border-box;
  2832. margin: 5px 0px 5px 0px;
  2833. border: 1px solid #00aeeccc;
  2834. box-shadow: 0 0 1px #0000004d;
  2835. }
  2836. .OJBetter_setting_menu textarea:focus-visible{
  2837. border-style: solid;
  2838. border-color: #3f51b5;
  2839. outline: none;
  2840. }
  2841. .OJBetter_setting_menu textarea::placeholder{
  2842. color: #BDBDBD;
  2843. font-size: 14px;
  2844. }
  2845. .OJBetter_setting_menu #tempConfig_save {
  2846. cursor: pointer;
  2847. display: inline-flex;
  2848. padding: 5px;
  2849. background-color: #1aa06d;
  2850. color: #ffffff;
  2851. font-size: 14px;
  2852. line-height: 1.5rem;
  2853. font-weight: 500;
  2854. justify-content: center;
  2855. width: 100%;
  2856. border-radius: 0.375rem;
  2857. border: none;
  2858. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  2859. margin-top: 20px
  2860. }
  2861. .OJBetter_setting_menu button#debug_button.debug_button {
  2862. width: 18%;
  2863. }
  2864. .OJBetter_setting_menu span.tip {
  2865. color: #999;
  2866. font-size: 12px;
  2867. font-weight: 500;
  2868. padding: 5px 0px;
  2869. }
  2870. /*设置面板-tip*/
  2871. .help_tip {
  2872. margin-right: auto;
  2873. }
  2874. span.input_label {
  2875. font-size: 14px;
  2876. }
  2877. .help_tip .tip_text {
  2878. display: none;
  2879. position: absolute;
  2880. color: #697e91;
  2881. font-weight: 400;
  2882. font-size: 14px;
  2883. letter-spacing: 0px;
  2884. background-color: #ffffff;
  2885. padding: 10px;
  2886. margin: 5px 0px;
  2887. border-radius: 4px;
  2888. border: 1px solid #e4e7ed;
  2889. box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
  2890. z-index: 100;
  2891. }
  2892. .help_tip .tip_text p {
  2893. margin-bottom: 5px;
  2894. }
  2895. .help_tip .tip_text:before {
  2896. content: "";
  2897. position: absolute;
  2898. top: -20px;
  2899. right: -10px;
  2900. bottom: -10px;
  2901. left: -10px;
  2902. z-index: -1;
  2903. }
  2904. .help-icon {
  2905. cursor: help;
  2906. width: 15px;
  2907. color: #b4b9d4;
  2908. margin-left: 5px;
  2909. margin-top: 3px;
  2910. }
  2911. .OJBetter_setting_menu .OJBetter_setting_menu_label_text .help_tip .help-icon {
  2912. color: #7fbeb2;
  2913. }
  2914. .help_tip .help-icon:hover + .tip_text, .help_tip .tip_text:hover {
  2915. display: block;
  2916. cursor: help;
  2917. width: 250px;
  2918. }
  2919. /* 版本信息 */
  2920. .OJBetter_setting_menu .versionInfo{
  2921. display: grid;
  2922. justify-items: center;
  2923. font-size: 16px;
  2924. padding: 10px;
  2925. }
  2926. .OJBetter_setting_menu .versionInfo>* {
  2927. margin: 10px 0px;
  2928. }
  2929.  
  2930. /* 配置管理面板 */
  2931. .config{
  2932. width: 100%;
  2933. margin: 10px 0px;
  2934. }
  2935. .config li.tempConfig_add_button {
  2936. cursor: pointer;
  2937. height: 40px;
  2938. border: 1px dashed #BDBDBD;
  2939. border-radius: 8px;
  2940. background-color: #fcfbfb36;
  2941. color: #bdbdbd;
  2942. font-size: 14px;
  2943. align-items: center;
  2944. justify-content: center;
  2945. }
  2946. .config li.tempConfig_add_button:hover {
  2947. border: 1px dashed #03A9F4;
  2948. background-color: #d7f0fb8c;
  2949. color: #03A9F4;
  2950. }
  2951. .config .config_bar_list {
  2952. display: flex;
  2953. width: 100%;
  2954. padding-bottom: 2px;
  2955. border: 1px solid #c5cae9;
  2956. background-color: #f0f8ff;
  2957. box-sizing: border-box;
  2958. border-radius: 0px 0px 8px 8px;
  2959. }
  2960. .config .config_bar_list input[type="radio"] {
  2961. appearance: none;
  2962. width: 0;
  2963. height: 0;
  2964. overflow: hidden;
  2965. }
  2966. .config .config_bar_list input[type="radio"] {
  2967. margin: 0px;
  2968. }
  2969. .config .config_bar_list input[type=radio]:focus {
  2970. outline: 0px;
  2971. }
  2972. .config .config_bar_ul_li_text {
  2973. display: flex;
  2974. align-items: center;
  2975. justify-content: center;
  2976. max-width: 100%;
  2977. height: 40px;
  2978. overflow-x: auto;
  2979. font-size: 14px;
  2980. font-weight: 400;
  2981. margin: 0px 4px;
  2982. padding: 3px;
  2983. border: 1px solid #dedede;
  2984. border-radius: 10px;
  2985. box-shadow: 0px 2px 4px 0px rgba(0,0,0,.05);
  2986. box-sizing: border-box;
  2987. }
  2988. .config .config_bar_ul li button {
  2989. background-color: #e6e6e6;
  2990. color: #727378;
  2991. height: 23px;
  2992. font-size: 14px;
  2993. border-radius: 0.3rem;
  2994. padding: 1px 5px;
  2995. margin: 5px;
  2996. border: none;
  2997. box-shadow: 0 0 1px #0000004d;
  2998. }
  2999. .config .config_bar_ul {
  3000. display: flex;
  3001. align-items: center;
  3002. list-style-type: none;
  3003. padding-inline-start: 0px;
  3004. overflow-x: auto;
  3005. max-width: 100%;
  3006. margin: 0px;
  3007. padding: 5px;
  3008. }
  3009. .config .config_bar_ul li {
  3010. width: 80px;
  3011. display: grid;
  3012. margin: 4px 4px;
  3013. min-width: 100px;
  3014. box-sizing: border-box;
  3015. }
  3016. .config .config_bar_ul_li_text:hover {
  3017. background-color: #eae4dc24;
  3018. }
  3019. input[type="radio"]:checked + .config_bar_ul_li_text {
  3020. background: #41b3e430;
  3021. border: 1px solid #5e7ce0;
  3022. color: #5e7ce0;
  3023. }
  3024. .config .config_bar_ul::-webkit-scrollbar {
  3025. width: 5px;
  3026. height: 4px;
  3027. }
  3028. .config .config_bar_ul::-webkit-scrollbar-thumb {
  3029. background-clip: padding-box;
  3030. background-color: #d7d9e4;
  3031. border-radius: 8px;
  3032. }
  3033. .config .config_bar_ul::-webkit-scrollbar-button:start:decrement {
  3034. width: 4px;
  3035. background-color: transparent;
  3036. }
  3037. .config .config_bar_ul::-webkit-scrollbar-button:end:increment {
  3038. width: 4px;
  3039. background-color: transparent;
  3040. }
  3041. .config .config_bar_ul::-webkit-scrollbar-track {
  3042. border-radius: 5px;
  3043. }
  3044. .config .config_bar_ul_li_text::-webkit-scrollbar {
  3045. width: 5px;
  3046. height: 7px;
  3047. background-color: #aaa;
  3048. }
  3049. .config .config_bar_ul_li_text::-webkit-scrollbar-thumb {
  3050. background-clip: padding-box;
  3051. background-color: #d7d9e4;
  3052. }
  3053. .config .config_bar_ul_li_text::-webkit-scrollbar-track {
  3054. background-color: #f1f1f1;
  3055. }
  3056. .config .config_bar_list_add_div {
  3057. display: flex;
  3058. height: 40px;
  3059. margin: 4px 2px;
  3060. }
  3061.  
  3062. /* 修改菜单 */
  3063. #config_bar_menu {
  3064. z-index: 400;
  3065. position: fixed;
  3066. width: 60px;
  3067. background: #ffffff;
  3068. box-shadow: 1px 1px 4px 0px #0000004d;
  3069. border: 0px solid rgba(0,0,0,0.04);
  3070. border-radius: 4px;
  3071. padding: 8px 0;
  3072. }
  3073. .config_bar_menu_item {
  3074. cursor: pointer;
  3075. padding: 2px 6px;
  3076. display: flex;
  3077. justify-content: center;
  3078. align-items: center;
  3079. height: 32px;
  3080. font-size: 14px;
  3081. font-weight: 500;
  3082. box-shadow: inset 0px 0px 0px 0px #8bb2d9;
  3083. }
  3084. #config_bar_menu_edit:hover {
  3085. background-color: #00aeec;
  3086. color: white;
  3087. }
  3088. #config_bar_menu_delete:hover {
  3089. background-color: #FF5722;
  3090. color: white;
  3091. }
  3092.  
  3093. /* 配置编辑页面 */
  3094. #config_edit_menu {
  3095. z-index: 300;
  3096. width: 450px;
  3097. }
  3098.  
  3099. /* 黑暗模式选项按钮 */
  3100. .dark-mode-selection {
  3101. display: flex;
  3102. justify-content: center;
  3103. align-items: center;
  3104. max-width: 350px;
  3105. -webkit-user-select: none;
  3106. -moz-user-select: none;
  3107. -ms-user-select: none;
  3108. user-select: none;
  3109. }
  3110. .dark-mode-selection label {
  3111. margin: 8px 0px 8px 8px;
  3112. }
  3113. .dark-mode-selection > * {
  3114. margin: 6px;
  3115. }
  3116. .dark-mode-selection .OJBetter_setting_menu_label_text {
  3117. border-radius: 8px;
  3118. margin-bottom: 0px;
  3119. }
  3120.  
  3121. /*确认弹窗*/
  3122. .OJBetter_modal {
  3123. z-index: 600;
  3124. display: grid;
  3125. position: fixed;
  3126. top: 50%;
  3127. left: 50%;
  3128. transform: translate(-50%, -50%);
  3129. font-size: 12px;
  3130. font-family: var(--vp-font-family-base);
  3131. width: max-content;
  3132. padding: 10px 20px;
  3133. box-shadow: 0px 0px 0px 4px #ffffff;
  3134. border-radius: 6px;
  3135. background-color: #f0f4f9;
  3136. border-collapse: collapse;
  3137. border: 1px solid #ffffff;
  3138. color: #697e91;
  3139. }
  3140. .OJBetter_modal h2 {
  3141. font-size: 1.6em;
  3142. font-weight: 700;
  3143. }
  3144. .OJBetter_modal .content{
  3145. white-space: nowrap;
  3146. max-height: 500px;
  3147. overflow-y: auto;
  3148. }
  3149. .OJBetter_modal .buttons{
  3150. display: flex;
  3151. padding-top: 15px;
  3152. }
  3153. .OJBetter_modal button {
  3154. display: inline-flex;
  3155. justify-content: center;
  3156. align-items: center;
  3157. line-height: 1;
  3158. white-space: nowrap;
  3159. cursor: pointer;
  3160. text-align: center;
  3161. box-sizing: border-box;
  3162. outline: none;
  3163. transition: .1s;
  3164. user-select: none;
  3165. vertical-align: middle;
  3166. -webkit-appearance: none;
  3167. height: 24px;
  3168. padding: 5px 11px;
  3169. margin-right: 15px;
  3170. font-size: 12px;
  3171. border-radius: 4px;
  3172. color: #ffffff;
  3173. background: #009688;
  3174. border-color: #009688;
  3175. border: none;
  3176. }
  3177. .OJBetter_modal button.secondary{
  3178. background-color:#4DB6AC;
  3179. }
  3180. .OJBetter_modal button:hover{
  3181. background-color:#4DB6AC;
  3182. }
  3183. .OJBetter_modal button.secondary:hover {
  3184. background-color: #80CBC4;
  3185. }
  3186. .OJBetter_modal .help-icon {
  3187. margin: 0px 8px 0px 0px;
  3188. height: 1em;
  3189. width: 1em;
  3190. line-height: 1em;
  3191. display: inline-flex;
  3192. justify-content: center;
  3193. align-items: center;
  3194. position: relative;
  3195. fill: currentColor;
  3196. font-size: inherit;
  3197. }
  3198. .OJBetter_modal p {
  3199. margin: 5px 0px;
  3200. }
  3201.  
  3202. /* 右键菜单 */
  3203. .OJBetter_contextmenu {
  3204. z-index: 500;
  3205. display: grid;
  3206. position: absolute;
  3207. background-color: #f0f4f9;
  3208. border-collapse: collapse;
  3209. color: #697e91;
  3210. font-family: var(--vp-font-family-base);
  3211. overflow: hidden;
  3212. box-sizing: content-box;
  3213. box-shadow: 0px 0px 0px 2px #eddbdb4d;
  3214. }
  3215. .OJBetter_contextmenu label {
  3216. margin: 0px;
  3217. }
  3218. input[type="radio"]:checked+.OJBetter_contextmenu_label_text {
  3219. background: #41e49930;
  3220. border: 1px solid green;
  3221. color: green;
  3222. font-weight: 500;
  3223. }
  3224. .OJBetter_contextmenu_label_text {
  3225. display: flex;
  3226. border: 1px dashed #80cbc4;
  3227. height: 26px;
  3228. width: 100%;
  3229. color: gray;
  3230. font-size: 13px;
  3231. font-weight: initial;
  3232. padding: 4px;
  3233. align-items: center;
  3234. -webkit-box-sizing: border-box;
  3235. -moz-box-sizing: border-box;
  3236. box-sizing: border-box;
  3237. }
  3238. .OJBetter_contextmenu_label_text:hover {
  3239. color: #F44336;
  3240. border: 1px dashed #009688;
  3241. background-color: #ffebcd;
  3242. }
  3243.  
  3244. /* RatingByClist */
  3245. .ratingBadge, html[data-theme=dark] button.ratingBadge{
  3246. display: block;
  3247. font-weight: 700;
  3248. font-size: 11px;
  3249. margin-top: 5px;
  3250. padding: 2px;
  3251. border-radius: 4px;
  3252. color: #B0BEC5;
  3253. border: 1px solid #cccccc66;
  3254. }
  3255.  
  3256. /* 多选翻译 */
  3257. .block_selected{
  3258. box-shadow: 0px 0px 0px 1px #FF9800;
  3259. outline: none;
  3260. }
  3261.  
  3262. /* 悬浮菜单 */
  3263. .OJBetter_MiniTranslateButton {
  3264. z-index: 100;
  3265. display: grid;
  3266. position: absolute;
  3267. border-collapse: collapse;
  3268. fill: #F57C00;
  3269. background-color: #FFF3E0;
  3270. overflow: hidden;
  3271. box-sizing: content-box;
  3272. box-shadow: 0px 0px 0px 2px #FFE0B2;
  3273. border-radius: 100%;
  3274. }
  3275. .OJBetter_MiniTranslateButton:hover {
  3276. cursor: pointer;
  3277. box-shadow: 0px 0px 0px 2px #FFB74D;
  3278. }
  3279.  
  3280. /* acmsguru划分块 */
  3281. .OJBetter_acmsguru {
  3282. margin: 0 0 1em!important;
  3283. }
  3284.  
  3285. /* 代码提交表单 */
  3286. #OJBetter_SubmitForm.input-output-copier:hover {
  3287. background-color: #ffffff00;
  3288. }
  3289. #OJBetter_SubmitForm input[type="number"] {
  3290. width: 40px;
  3291. color: #009688;
  3292. appearance: none;
  3293. border-radius: 6px;
  3294. border-style: solid;
  3295. border: none;
  3296. background-color: #ffffff00;
  3297. }
  3298. #OJBetter_SubmitForm :focus-visible {
  3299. outline: none;
  3300. }
  3301. #OJBetter_SubmitForm .topDiv {
  3302. height: 50px;
  3303. display: flex;
  3304. align-items: center;
  3305. justify-content: space-between;
  3306. padding: 10px 0px;
  3307. box-sizing: border-box;
  3308. }
  3309. #OJBetter_SubmitForm .topDiv .topRightDiv {
  3310. height: 100%;
  3311. display: flex;
  3312. flex-wrap: wrap;
  3313. gap: 0px;
  3314. }
  3315. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3316. margin: 0px;
  3317. font-weight: initial;
  3318. }
  3319. #OJBetter_SubmitForm #fontSizeInput {
  3320. border: none;
  3321. background-color: #ffffff00;
  3322. }
  3323.  
  3324. /* 顶部区域 */
  3325. #OJBetter_SubmitForm .topRightDiv>* {
  3326. height: 100%;
  3327. box-sizing: border-box;
  3328. }
  3329. #OJBetter_SubmitForm .topRightDiv>button{
  3330. padding: 0px 8px;
  3331. }
  3332. #OJBetter_SubmitForm .topRightDiv {
  3333. display: flex;
  3334. flex-wrap: wrap;
  3335. gap: 0px;
  3336. align-items: center;
  3337. }
  3338. #OJBetter_SubmitForm input[type="checkbox"], #OJBetter_SubmitForm label {
  3339. margin: 0px;
  3340. font-weight: initial;
  3341. }
  3342.  
  3343. /* LSP连接Log */
  3344. #LSPLog{
  3345. width: 500px;
  3346. height: 500px;
  3347. position: fixed;
  3348. top: 50%;
  3349. left: 50%;
  3350. padding: 10px;
  3351. transform: translate(-50%, -50%);
  3352. border: 1px solid;
  3353. z-index: 200;
  3354. background-color: #ffffff;
  3355. }
  3356. #LSPLog button{
  3357. position: fixed;
  3358. top: 10px;
  3359. right: 10px;
  3360. z-index: 200;
  3361. }
  3362. #LSPLog #LSPLogList{
  3363. width: 500px;
  3364. height: 500px;
  3365. overflow: auto;
  3366. color: #424242;
  3367. }
  3368. #LSPLog li:nth-child(odd){
  3369. background-color: #f5f5f5;
  3370. }
  3371. #LSPLog details{
  3372. padding: 2px;
  3373. }
  3374.  
  3375. /* 代码编辑器 */
  3376. #OJBetter_editor{
  3377. box-sizing: border-box;
  3378. height: 600px;
  3379. border: 1px solid #d3d3d3;
  3380. width: 100%;
  3381. resize: vertical;
  3382. display: flex;
  3383. flex-direction: column;
  3384. }
  3385. #OJBetter_editor.fullscreen{
  3386. position: fixed;
  3387. top: 0;
  3388. left: 0;
  3389. width: 100%;
  3390. height: 100vh;
  3391. z-index: 2000;
  3392. }
  3393. #OJBetter_editor.bottom{
  3394. position: fixed;
  3395. bottom: 0;
  3396. left: 0;
  3397. width: 100%;
  3398. height: 50vh;
  3399. z-index: 2000;
  3400. }
  3401. .ojb_btn.exit_button_bottom {
  3402. position: fixed;
  3403. bottom: 30px;
  3404. right: 15px;
  3405. z-index: 2000;
  3406. height: 28px;
  3407. }
  3408.  
  3409. /* monaco */
  3410. #OJBetter_monaco {
  3411. flex: 1;
  3412. min-height: 0;
  3413. width: 100%;
  3414. }
  3415. #OJBetter_monaco .highlight {
  3416. border: 1px solid #ffffff00;
  3417. background-color: #ffffff00!important
  3418. }
  3419. .monaco-hover hr {
  3420. margin: 4px -8px 4px !important;
  3421. }
  3422.  
  3423. /* 状态底栏 */
  3424. #OJBetter_statusBar{
  3425. height: 22px;
  3426. font-size: 12px;
  3427. color: #757575;
  3428. border: 1px solid #d3d3d3;
  3429. background-color: #f8f8f8;
  3430. padding: 3px;
  3431. box-sizing: border-box;
  3432. }
  3433.  
  3434. /* 提交 */
  3435. #OJBetter_submitDiv{
  3436. display: flex;
  3437. padding-top: 15px;
  3438. height: 50px;
  3439. box-sizing: border-box;
  3440. }
  3441. #OJBetter_submitDiv >* {
  3442. border-radius: 6px;
  3443. }
  3444. #OJBetter_submitDiv > button {
  3445. height: 100%;
  3446. aspect-ratio: 1 / 1;
  3447. }
  3448. #SubmitButton {
  3449. color: #fff;
  3450. background-color: #209978;
  3451. border-color: #17795E;
  3452. }
  3453. #SubmitButton:hover {
  3454. background-color: #17795e;
  3455. }
  3456. #SubmitButton.disabled {
  3457. background-color: red;
  3458. animation: shake 0.07s infinite alternate;
  3459. }
  3460. #programTypeId{
  3461. height: 100%;
  3462. padding: 5px 10px;
  3463. border-radius: 6px;
  3464. border-style: solid;
  3465. border: 1px solid #ced4da;
  3466. color: #212529;
  3467. }
  3468.  
  3469. /* 调试 */
  3470. .OJBetter_loding{
  3471. padding: 6px 0px 0px 5px;
  3472. height: 22px;
  3473. }
  3474. #CompilerArgsInput{
  3475. flex-grow: 1;
  3476. width: 100%;
  3477. height: 100%;
  3478. margin-bottom: 10px;
  3479. padding: 5px 10px;
  3480. border-radius: 6px;
  3481. box-sizing: border-box;
  3482. border: 1px solid #ccc;
  3483. box-shadow: inset 0px 1px 1px rgba(0,0,0,.075);
  3484. }
  3485. #CompilerArgsInput[disabled] {
  3486. cursor: not-allowed;
  3487. }
  3488. #CompilerSetting{
  3489. font-size: 14px;
  3490. margin-top: 10px;
  3491. display: none;
  3492. }
  3493. #CompilerSetting select, #CompilerSetting textarea{
  3494. padding: 4px 10px;
  3495. border-radius: 6px;
  3496. border-style: solid;
  3497. border: 1px solid #ced4da;
  3498. color: #212529;
  3499. }
  3500. #CompilerBox{
  3501. display: grid;
  3502. margin-top: 10px;
  3503. border: #d0d7de solid 1px;
  3504. border-radius: 6px;
  3505. }
  3506. #CompilerBox > * {
  3507. margin: 5px;
  3508. }
  3509.  
  3510. /* 自定义样例 */
  3511. #customTestBlock {
  3512. margin-top: 10px;
  3513. font-size: 14px;
  3514. color: #616161;
  3515. border: 1px solid #d3d3d3;
  3516. box-sizing: border-box;
  3517. position: relative;
  3518. }
  3519. #customTestBlock #customTests{
  3520. border-top: 1px solid #d3d3d3;
  3521. margin: 0px 0px 40px 0px;
  3522. }
  3523. #customTestBlock summary {
  3524. cursor: pointer;
  3525. padding: 10px;
  3526. }
  3527. #customTestBlock textarea {
  3528. resize: vertical;
  3529. }
  3530. .sampleDiv {
  3531. color: #727378;
  3532. background-color: #FAFAFA;
  3533. padding: 5px;
  3534. margin-bottom: 10px;
  3535. box-shadow: inset 0 0 1px #0000004d;
  3536. position: relative;
  3537. }
  3538. .dynamicTextarea {
  3539. width: 98%;
  3540. height: 120px;
  3541. margin: 10px 5px;
  3542. border: 1px solid #E0E0E0;
  3543. }
  3544. .deleteCustomTest {
  3545. cursor: pointer;
  3546. position: absolute;
  3547. top: 5px;
  3548. right: 5px;
  3549. display: flex;
  3550. fill: #9E9E9E;
  3551. padding: 2px 2px;
  3552. border-radius: 4px;
  3553. border: 1px solid #ffffff00;
  3554. background-color: #ffffff00;
  3555. align-items: center;
  3556. }
  3557. .deleteCustomTest:hover {
  3558. fill: #EF5350;
  3559. border: 1px solid #ef9a9a;
  3560. background-color: #FFEBEE;
  3561. }
  3562. #addCustomTest {
  3563. cursor: pointer;
  3564. position: absolute;
  3565. bottom: 5px;
  3566. right: 5px;
  3567. padding: 3px 10px;
  3568. color: #795548;
  3569. border: 1px solid #ccc;
  3570. border-radius: 4px;
  3571. background-color: #FAFAFA;
  3572. }
  3573. #addCustomTest:hover {
  3574. background-color: #f5f5f5;
  3575. }
  3576.  
  3577. /* 调试结果 */
  3578. #statePanel{
  3579. display: none;
  3580. padding: 5px;
  3581. margin-top: 10px;
  3582. border: 1px solid #ddd;
  3583. border-radius: 4px;
  3584. }
  3585. .test-case {
  3586. padding: 10px;
  3587. border: 1px solid #ddd;
  3588. border-radius: 4px;
  3589. }
  3590. .test-case:not(:first-child){
  3591. margin-top: 5px;
  3592. }
  3593. .test-case > * {
  3594. margin: 5px 0px;
  3595. }
  3596. .test-case > :first-child {
  3597. margin-top: 0px;
  3598. }
  3599. .test-case > :last-child {
  3600. margin-bottom: 0px;
  3601. }
  3602. .test-case-title, .test-case-status {
  3603. font-size: 16px;
  3604. display: inline;
  3605. }
  3606. .test-case-status{
  3607. margin-left: 5px;
  3608. }
  3609. .test-case-status.error{
  3610. color: red;
  3611. }
  3612. .test-case-status.success{
  3613. color: #449d44;
  3614. }
  3615. .test-case-judge {
  3616. font-size: 13px;
  3617. }
  3618.  
  3619. /* 差异对比 */
  3620. .output_diff {
  3621. color: #5d4037;
  3622. margin: 5px 0px;
  3623. display: grid;
  3624. border: 1px solid #bcaaa4;
  3625. font-size: 13px;
  3626. font-family: Consolas, "Lucida Console", "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
  3627. overflow: auto;
  3628. }
  3629. .output_diff .added {
  3630. background-color: #c8f7c5;
  3631. user-select: none;
  3632. }
  3633. .output_diff .removed {
  3634. background-color: #f7c5c5;
  3635. }
  3636. .output_diff .diffLine {
  3637. display: flex;
  3638. }
  3639. .output_diff .diffLine:nth-child(odd) {
  3640. background-color: #f5f5f5;
  3641. }
  3642. .lineNo {
  3643. display: flex;
  3644. align-items: center;
  3645. justify-content: center;
  3646. width: 17px;
  3647. color: #BDBDBD;
  3648. font-size: 10px;
  3649. border-right: 1px solid;
  3650. user-select: none;
  3651. }
  3652. .lineContent {
  3653. display: grid;
  3654. width: 100%;
  3655. }
  3656. .lineContent>span {
  3657. height: 16px;
  3658. padding-left: 3px;
  3659. }
  3660. .output_no_diff {
  3661. padding: 5px;
  3662. border: 1px solid #ddd;
  3663. }
  3664. .diff_note {
  3665. font-size: 10px;
  3666. }
  3667.  
  3668. /* 网站本地化替换规则标记 */
  3669. .markingTextReplaceRule{
  3670. color: #FFF3E0;
  3671. background-color: #FF9800;
  3672. }
  3673.  
  3674. /* SelectPage样式 */
  3675. .sp_input {
  3676. padding: 4px 6px px !important;
  3677. height: 20px !important;
  3678. min-height: 20px !important;
  3679. line-height: 20px !important;
  3680. }
  3681. div.sp_clear_btn {
  3682. padding: 0px !important;
  3683. }
  3684.  
  3685. /* 题目问题标签hover隐藏 */
  3686. .hover-reveal {
  3687. opacity: 0;
  3688. transition: opacity 0.5s;
  3689. }
  3690. .hover-reveal:hover {
  3691. opacity: 1;
  3692. }
  3693.  
  3694.  
  3695. /* 移动设备 */
  3696. @media (max-device-width: 450px) {
  3697. .ojb_btn{
  3698. height: 2em;
  3699. font-size: 1.2em;
  3700. }
  3701. .ojb_btn.OJBetter_setting{
  3702. height: 2.5em;
  3703. font-size: 1em;
  3704. }
  3705. .OJBetter_setting_menu{
  3706. width: 90%;
  3707. }
  3708. .OJBetter_setting_menu label, #darkMode_span, #loaded_span, .OJBetter_setting_menu_label_text,
  3709. .OJBetter_setting_sidebar li{
  3710. font-size: 1em;
  3711. }
  3712. .translate-problem-statement{
  3713. font-size: 1.2em;
  3714. }
  3715. .OJBetter_modal{
  3716. font-size: 1.5em;
  3717. }
  3718. .OJBetter_setting_list, .translate-problem-statement{
  3719. padding: 0.5em;
  3720. }
  3721. .OJBetter_setting_menu_label_text{
  3722. height: 2.5em;
  3723. padding: 0.5em;
  3724. }
  3725. #pagBar #jump-input, #pagBar #items-per-page, .OJBetter_modal button{
  3726. height: 2.5em;
  3727. font-size: 1em;
  3728. }
  3729. .translate-problem-statement p, .translate-problem-statement ul li{
  3730. line-height: 1.5em !important;
  3731. }
  3732. .OJBetter_contextmenu_label_text{
  3733. height: 3em;
  3734. font-size: 1em;
  3735. }
  3736. }
  3737.  
  3738. /* 覆盖网站原本的样式 */
  3739. #footer > div:nth-child(7) {
  3740. left: 0px !important;
  3741. }
  3742. `);
  3743.  
  3744. /**
  3745. * 添加一些依赖库和条件加载的css样式
  3746. */
  3747. function addDependencyStyles() {
  3748. GM_addStyle(GM_getResourceText("xtermcss"));
  3749. GM_addStyle(GM_getResourceText("selectpagecss"));
  3750. GM_addStyle(GM_getResourceText("dialogpolyfillcss"));
  3751. // 自定义图标大小
  3752. GM_addStyle(`
  3753. .iconfont {
  3754. font-size: ${OJBetter.preference.iconButtonSize}px;
  3755. }
  3756. `);
  3757. }
  3758.  
  3759. /**
  3760. * 添加包含i18n内容的css样式
  3761. */
  3762. function addI18nStyles() {
  3763. GM_addStyle(`
  3764. /* 加载鼠标悬浮覆盖层css */
  3765. .overlay::before {
  3766. content: '';
  3767. position: absolute;
  3768. top: 0;
  3769. left: 0;
  3770. width: 100%;
  3771. height: 100%;
  3772. background: repeating-linear-gradient(135deg, rgb(77 208 225 / 30%), rgb(77 208 225 / 30%) 30px, rgb(77 208 225 / 10%) 0px, rgb(77 208 225 / 10%) 55px);
  3773. z-index: 100;
  3774. }
  3775. .overlay::after {
  3776. content: '${i18next.t('targetArea', { ns: 'common' })}';
  3777. position: absolute;
  3778. top: 50%;
  3779. left: 50%;
  3780. transform: translate(-50%, -50%);
  3781. color: #00695C;
  3782. font-size: 16px;
  3783. font-weight: bold;
  3784. z-index: 100;
  3785. }
  3786.  
  3787. .config::before {
  3788. content: "${i18next.t('common.configManageTitle', { ns: 'settings' })}";
  3789. display: block;
  3790. height: 20px;
  3791. background-color: #f0f8ff;
  3792. border: 1px solid #c5cae9;
  3793. border-bottom: 0px;
  3794. line-height: 20px;
  3795. padding: 2px 10px;
  3796. border-radius: 8px 8px 0px 0px;
  3797. box-sizing: content-box;
  3798. }
  3799. .config.missing::before {
  3800. content: "${i18next.t('common.missing.radio', { ns: 'settings' })}";
  3801. background-color: #fef0f0;
  3802. color: #f56c6c;
  3803. border: 1px solid #fab6b6;
  3804. }
  3805. `);
  3806. }
  3807.  
  3808. // ------------------------------
  3809. // 一些工具类
  3810. // ------------------------------
  3811.  
  3812. /**
  3813. * 自定义错误类,以区分不同的错误类型
  3814. */
  3815. class OJB_GMError extends Error {
  3816. constructor(type, message, originalError) {
  3817. super(message);
  3818. this.name = 'GMError';
  3819. this.type = type;
  3820. this.stack = originalError.stack;
  3821. Object.assign(this, originalError);
  3822. }
  3823. }
  3824.  
  3825. /**
  3826. * 文本块替换/恢复类
  3827. */
  3828. class TextBlockReplacer {
  3829. constructor() {
  3830. /** @type {string[]} 匹配项 */
  3831. this.matches = [];
  3832. /** @type {Map<string, string>} 待还原项 */
  3833. this.replacements = new Map();
  3834. /** @type {Map<string, string>} 暂时未找到的待还原项 */
  3835. this.tempReplacements = new Map();
  3836. /** @type {string} 替换符号 */
  3837. this.replaceSymbol = OJBetter.translation.replaceSymbol;
  3838. }
  3839.  
  3840. /**
  3841. * 替换文本
  3842. * @param {string} text 原文本
  3843. * @param {RegExp} regex 匹配规则
  3844. * @returns {string} 替换后的文本
  3845. */
  3846. replace(text, regex) {
  3847. this.matches = text.match(regex) || [];
  3848. try {
  3849. for (let i = 0; i < this.matches.length; i++) {
  3850. const match = this.matches[i];
  3851. const id = OJB_getRandomNumber(8);
  3852. let replacement = '';
  3853. switch (this.replaceSymbol) {
  3854. case "1":
  3855. replacement = `【${id}】`;
  3856. break;
  3857. case "2":
  3858. replacement = `{${id}}`;
  3859. break;
  3860. case "3":
  3861. replacement = `[${id}]`;
  3862. break;
  3863. default:
  3864. replacement = `【${id}】`;
  3865. break;
  3866. }
  3867. text = text.replace(match, replacement);
  3868. this.replacements.set(id, match);
  3869. }
  3870. } catch (e) { }
  3871. return text;
  3872. }
  3873.  
  3874.  
  3875. /**
  3876. * 恢复替换的文本
  3877. * @param {string} text 还原前的文本
  3878. * @returns {string} 还原后的文本
  3879. */
  3880. recover(text) {
  3881. let textCopy = text;
  3882.  
  3883. /**
  3884. * 替换回文本
  3885. * @param {string} replacement 替换的文本
  3886. * @param {string} regexPattern 匹配规则
  3887. * @returns {void}
  3888. */
  3889. const replaceText = (replacement, regexPattern) => {
  3890. const latexMatch = '(?<latex_block>\\$\\$(\\\\\\$|[^\\$])*?\\$\\$)|(?<latex_inline>\\$(\\\\\\$|[^\\$])*?\\$)|';
  3891. const regex = new RegExp(latexMatch + regexPattern, 'g');
  3892. textCopy = textCopy.replace(regex, (match, ...args) => {
  3893. // LaTeX中的不替换
  3894. const groups = args[args.length - 1]; // groups是replace方法的最后一个参数
  3895. if (groups.latex_block || groups.latex_inline) return match;
  3896. // 没有空格则加一个
  3897. const offset = args[args.length - 3]; // offset是replace方法的倒数第三个参数
  3898. let leftSpace = "", rightSpace = "";
  3899. if (!/\s/.test(textCopy[offset - 1])) leftSpace = " ";
  3900. if (!/\s/.test(textCopy[offset + match.length])) rightSpace = " ";
  3901. return leftSpace + replacement + rightSpace;
  3902. });
  3903. };
  3904.  
  3905. /**
  3906. * 尝试还原
  3907. * @param {string} replacement 替换的文本
  3908. * @param {string} id 替换的 id
  3909. * @returns {boolean} 是否替换成功
  3910. */
  3911. const tryRecover = (replacement, id) => {
  3912. // 尝试还原,如果还原成功,则从 replacements 中删除
  3913. const originalText = textCopy;
  3914. replaceText(replacement, `【\\s*${id}\\s*】|\\[\\s*${id}\\s*\\]|{\\s*${id}\\s*}`); // 替换符完整匹配(考虑了多出空格的情况)
  3915. replaceText(replacement, `【\\s*${id}(?![】\\d])|(?<![【\\d])${id}\\s*】|\\[\\s*${id}(?![\\]\\d])|(?<![\\[\\d])${id}\\s*\\]|{\\s*${id}(?![}\\d])|(?<![{\\d])${id}\\s*}`); // 替换符部分匹配
  3916.  
  3917. if (textCopy === originalText) {
  3918. // 如果文本没有变化,说明没有找到,加入到 tempReplacements
  3919. this.tempReplacements.set(id, replacement);
  3920. return false;
  3921. } else {
  3922. // 如果文本变化了,说明找到并成功替换,则删除
  3923. this.replacements.delete(id);
  3924. this.tempReplacements.delete(id);
  3925. return true;
  3926. }
  3927. }
  3928.  
  3929. // 处理 replacements 中的项
  3930. this.replacements.forEach((replacement, id) => {
  3931. tryRecover(replacement, id);
  3932. });
  3933.  
  3934. // 处理 tempReplacements 中的项
  3935. while (this.tempReplacements.size > 0) {
  3936. let found = false;
  3937. this.tempReplacements.forEach((replacement, id) => {
  3938. found = tryRecover(replacement, id) || found;
  3939. });
  3940. if (!found) break; // 如果这一轮没有找到任何项,终止循环
  3941. }
  3942.  
  3943. // 如果 tempReplacements 还有未找到的项
  3944. if (this.tempReplacements.size > 0) {
  3945. console.warn("There are still some replacements not found:", this.tempReplacements);
  3946. }
  3947.  
  3948. return textCopy;
  3949. }
  3950. }
  3951.  
  3952. // ------------------------------
  3953. // 一些工具函数
  3954. // ------------------------------
  3955.  
  3956. /**
  3957. * 格式化链接格式
  3958. * @param {string} url 链接字符串
  3959. * @returns {string} 清理后的链接字符串
  3960. */
  3961. function OJB_cleanLink(url) {
  3962. if (url === null || url === undefined) return "";
  3963.  
  3964. // 替换'http://'为'https://'
  3965. let cleanUrl = url.replace(/^http:\/\//i, 'https://');
  3966.  
  3967. // 移除末尾的斜杠
  3968. cleanUrl = cleanUrl.replace(/\/$/, '');
  3969.  
  3970. return cleanUrl;
  3971. }
  3972.  
  3973. /**
  3974. * 深度比较两个对象或数组是否完全相等。
  3975. * @param {any} a - 第一个比较对象。
  3976. * @param {any} b - 第二个比较对象。
  3977. * @returns {boolean} - 如果两个对象或数组深度相等,则返回true,否则返回false。
  3978. */
  3979. function OJB_deepEquals(a, b) {
  3980. if (a === b) return true;
  3981. if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
  3982. const keysA = Object.keys(a);
  3983. const keysB = Object.keys(b);
  3984. if (keysA.length !== keysB.length) return false;
  3985. for (let key of keysA) {
  3986. if (!b.hasOwnProperty(key)) return false;
  3987. if (!OJB_deepEquals(a[key], b[key])) return false;
  3988. }
  3989. return true;
  3990. }
  3991.  
  3992. /**
  3993. * 用于封装需要重试的异步函数
  3994. * @param {Function} task 需要封装的异步函数
  3995. * @param {Object} options 配置项
  3996. * @param {Number} options.maxRetries 重试次数,默认为 5
  3997. * @param {Number} options.retryInterval 重试时间间隔,默认为 0 毫秒
  3998. * @param {Function} options.errorHandler 错误处理函数,默认为抛出错误
  3999. * @param {...any} args task 函数的参数
  4000. * @returns {Promise} 返回 Promise
  4001. */
  4002. async function OJB_promiseRetryWrapper(task, {
  4003. maxRetries = 5,
  4004. retryInterval = 0,
  4005. errorHandler = (err) => { throw err }
  4006. } = {}, ...args) {
  4007. let attemptsLeft = maxRetries;
  4008. while (attemptsLeft--) {
  4009. try {
  4010. return await task(...args);
  4011. } catch (err) {
  4012. if (attemptsLeft <= 0) {
  4013. return errorHandler(err, maxRetries, attemptsLeft);
  4014. }
  4015. if (retryInterval > 0) {
  4016. await OJB_delay(retryInterval);
  4017. }
  4018. }
  4019. }
  4020. }
  4021.  
  4022. /**
  4023. * GM_xmlhttpRequest 的 Promise 封装
  4024. * @param {Object} options GM_xmlhttpRequest 的参数
  4025. * @param {Boolean} isStream 是否为流式请求
  4026. * @returns {Promise<OJB_GMError>} 返回 Promise
  4027. */
  4028. function OJB_GMRequest(options, isStream = false) {
  4029. return new Promise((resolve, reject) => {
  4030. GM_xmlhttpRequest({
  4031. ...options,
  4032. ...(isStream ? {
  4033. onloadstart: resolve
  4034. } : {
  4035. onload: resolve
  4036. }),
  4037. onerror: (error) => reject(new OJB_GMError('error', 'An error occurred during the request.', error)),
  4038. ontimeout: (error) => reject(new OJB_GMError('timeout', 'The request timed out.', error)),
  4039. onabort: (error) => reject(new OJB_GMError('abort', 'The request was aborted.', error)),
  4040. });
  4041. });
  4042. }
  4043.  
  4044. /**
  4045. * 获取cookie
  4046. * @param {string} name cookie名称
  4047. * @returns {string} cookie值
  4048. */
  4049. function OJB_getCookie(name) {
  4050. const cookies = document.cookie.split(";");
  4051. for (let i = 0; i < cookies.length; i++) {
  4052. const cookie = cookies[i].trim();
  4053. const [cookieName, cookieValue] = cookie.split("=");
  4054.  
  4055. if (cookieName === name) {
  4056. return decodeURIComponent(cookieValue);
  4057. }
  4058. }
  4059. return "";
  4060. }
  4061.  
  4062. /**
  4063. * 检查是否仍在同一浏览器会话中
  4064. * @param {string} sessionKey - 会话键名,用于标识会话
  4065. * @returns {boolean} - 如果在当前会话中之前已经设置过这个键,则返回true,否则返回false
  4066. */
  4067. function OJB_isSameBrowserSession(sessionKey) {
  4068. const fullCookieName = `OJB_Session_${sessionKey}`;
  4069. const sessionValue = OJB_getCookie(fullCookieName);
  4070. if (sessionValue === "") {
  4071. document.cookie = `${fullCookieName}=true; path=/`;
  4072. return false;
  4073. }
  4074. return true;
  4075. }
  4076.  
  4077. /**
  4078. * 随机数生成
  4079. * @param {number} numDigits 位数
  4080. * @returns {number} 一个随机数
  4081. */
  4082. function OJB_getRandomNumber(numDigits) {
  4083. let min = Math.pow(10, numDigits - 1);
  4084. let max = Math.pow(10, numDigits) - 1;
  4085. return Math.floor(Math.random() * (max - min + 1)) + min;
  4086. }
  4087.  
  4088. /**
  4089. * 随机数生成-范围内
  4090. * @param {number} min 最小值
  4091. * @param {number} max 最大值
  4092. * @returns {number} 一个随机数
  4093. */
  4094. function OJB_getRandomNumberInRange(min, max) {
  4095. return Math.floor(Math.random() * (max - min + 1)) + min;
  4096. }
  4097.  
  4098. /**
  4099. * 防抖函数
  4100. * @param {Function} callback 回调函数
  4101. * @returns {Function}
  4102. */
  4103. function OJB_debounce(callback) {
  4104. let timer;
  4105. let immediateExecuted = false;
  4106. const delay = 500;
  4107. return function () {
  4108. clearTimeout(timer);
  4109. if (!immediateExecuted) { callback.call(this); immediateExecuted = true; }
  4110. timer = setTimeout(() => { immediateExecuted = false; }, delay);
  4111. };
  4112. }
  4113.  
  4114. /**
  4115. * 为元素添加鼠标拖拽支持
  4116. * @param {JQuery<HTMLElement>} element 要添加拖拽支持的元素
  4117. * @returns {void}
  4118. */
  4119. function OJB_addDraggable(element) {
  4120. let isDragging = false;
  4121. let x, y, l, t, nl, nt;
  4122. let isSpecialMouseDown = false; // 选取某些元素时不拖动
  4123.  
  4124. element.on('mousedown', function (e) {
  4125. isSpecialMouseDown = $(e.target).is('label, p, input, textarea, span, select, details, summary');
  4126. if (isSpecialMouseDown) return;
  4127.  
  4128. isDragging = true;
  4129. x = e.clientX;
  4130. y = e.clientY;
  4131. l = element.offset().left - $(window).scrollLeft();
  4132. t = element.offset().top - $(window).scrollTop();
  4133.  
  4134. element.css({ left: l + 'px', top: t + 'px', transform: 'none' });
  4135.  
  4136. $(document).on("mousemove", drag);
  4137. $(document).on("mouseup", stopDrag);
  4138. element.css('cursor', 'all-scroll');
  4139. });
  4140.  
  4141. const drag = (e) => {
  4142. if (!isDragging) return;
  4143. // 不执行拖动操作
  4144. if ($(e.target).is('label, p, input, textarea, span') || isSpecialMouseDown && !$(e.target).is('input, textarea')) return;
  4145. e.preventDefault();
  4146.  
  4147. const nx = e.clientX;
  4148. const ny = e.clientY;
  4149. nl = nx - (x - l);
  4150. nt = ny - (y - t);
  4151. element.css({ transform: `translate(${nx - x}px, ${ny - y}px)` });
  4152. };
  4153.  
  4154. const stopDrag = () => {
  4155. isDragging = false;
  4156. isSpecialMouseDown = false;
  4157. element.css('cursor', 'default');
  4158.  
  4159. // 在停止拖拽后,设置元素的left和top,并还原transform
  4160. element.css({ left: nl + 'px', top: nt + 'px', transform: 'none' });
  4161. $(document).off("mousemove", drag);
  4162. $(document).off("mouseup", stopDrag);
  4163. };
  4164. }
  4165.  
  4166. /**
  4167. * 切换元素的折叠/展开过渡动画
  4168. * @param {HTMLElement} element
  4169. */
  4170. function OJB_toggleCollapseExpand(element) {
  4171. // 设置transitionend事件监听器的函数
  4172. const setTransitionListener = (listener) => {
  4173. const listenerName = `transitionEndListener${Date.now()}`;
  4174. window[listenerName] = listener;
  4175. element.addEventListener('transitionend', listener);
  4176. element.setAttribute('data-transition-end-listener', listenerName);
  4177. };
  4178.  
  4179. // 移除事件监听器的函数
  4180. const removeTransitionListener = () => {
  4181. const transitionEndListenerName = element.getAttribute('data-transition-end-listener');
  4182. if (transitionEndListenerName) {
  4183. element.removeEventListener('transitionend', window[transitionEndListenerName]);
  4184. element.removeAttribute('data-transition-end-listener');
  4185. }
  4186. };
  4187.  
  4188. const collapsed = element.getAttribute('data-collapsed') === 'true';
  4189. const sectionHeight = element.scrollHeight;
  4190.  
  4191. // 移除事件监听器
  4192. removeTransitionListener();
  4193.  
  4194. // 设置初始样式
  4195. element.style.overflow = 'hidden';
  4196. element.style.transition = 'height 0.3s ease-out 0s';
  4197. element.style.height = collapsed ? `0px` : `${sectionHeight}px`;
  4198. element.style.opacity = collapsed ? '' : '1';
  4199.  
  4200. // 需要立即开始动画
  4201. requestAnimationFrame(() => {
  4202. // 设置结束样式
  4203. element.style.height = collapsed ? `${sectionHeight}px` : `0px`;
  4204. });
  4205.  
  4206. const transitionEndListener = (event) => {
  4207. if (event.propertyName === 'height') {
  4208. if (collapsed) {
  4209. // 展开后的设置
  4210. element.style.height = '';
  4211. element.style.overflow = '';
  4212. } else {
  4213. // 折叠后的设置
  4214. element.style.opacity = '0';
  4215. }
  4216. removeTransitionListener();
  4217. }
  4218. };
  4219.  
  4220. setTransitionListener(transitionEndListener);
  4221.  
  4222. // 更新data-collapsed属性
  4223. element.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
  4224. }
  4225.  
  4226. /**
  4227. * 获取外部JSON并转换为Object
  4228. * @param {string} url JSON Url
  4229. * @param {boolean} [nacache=true] 是否不使用缓存
  4230. * @returns {Promise<Object>} JSON Object
  4231. */
  4232. async function OJB_getExternalJSON(url, nacache = true) {
  4233. const response = await OJB_GMRequest({
  4234. method: "GET",
  4235. url: url,
  4236. nocache: nacache
  4237. });
  4238. try {
  4239. return JSON.parse(response.responseText);
  4240. } catch (e) {
  4241. throw new Error(`JSON parse error\n${e}`);
  4242. }
  4243. }
  4244.  
  4245. /**
  4246. * 创建确认对话框dialog
  4247. * @param {string} title 标题
  4248. * @param {string} content 内容
  4249. * @param {string[]} buttons 按钮 (取消 确定) 可以为null
  4250. * @param {boolean} renderMarkdown 是否使用markdown渲染文本
  4251. * @returns {Promise<boolean>} 用户点击了确定按钮返回true, 否则返回false
  4252. */
  4253. function OJB_createDialog(title, content, buttons, renderMarkdown = false) {
  4254. return new Promise(resolve => {
  4255. let contentHtml = content;
  4256.  
  4257. if (renderMarkdown) {
  4258. const md = window.markdownit();
  4259. contentHtml = md.render(content);
  4260. }
  4261.  
  4262. const dialog = OJB_safeCreateJQElement(`
  4263. <dialog class="OJBetter_modal">
  4264. <h2>${title}</h2>
  4265. <div class="content">${contentHtml}</div>
  4266. </dialog>
  4267. `);
  4268. const buttonbox = OJB_safeCreateJQElement(`<div class="buttons"></div>`);
  4269. const cancelButton = OJB_safeCreateJQElement(`<button class="cancelButton">${buttons[0]}</button>`)
  4270. .addClass("secondary");
  4271. const continueButton = OJB_safeCreateJQElement(`<button class="continueButton">${buttons[1]}</button>`);
  4272. if (buttons[0] !== null) buttonbox.append(cancelButton);
  4273. if (buttons[1] !== null) buttonbox.append(continueButton);
  4274. dialog.append(buttonbox);
  4275. $('body').before(dialog);
  4276.  
  4277. OJB_showModal(dialog);
  4278. OJB_addDraggable(dialog);
  4279.  
  4280. continueButton.click(function () {
  4281. OJB_closeAndRemoveModal(dialog);
  4282. resolve(true);
  4283. });
  4284.  
  4285. cancelButton.click(function () {
  4286. OJB_closeAndRemoveModal(dialog);
  4287. resolve(false);
  4288. });
  4289. });
  4290. }
  4291.  
  4292. /**
  4293. * 显示模态对话框并阻止页面滚动,同时考虑滚动条宽度变化和原始marginRight
  4294. * @param {JQuery<HTMLElement>} element
  4295. */
  4296. function OJB_showModal(element) {
  4297. const dialog = element.get(0);
  4298. dialogPolyfill.registerDialog(dialog);
  4299. dialog.showModal();
  4300. OJBetter.state.openDialogCount++;
  4301.  
  4302. if (OJBetter.state.openDialogCount === 1) {
  4303. const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
  4304. // 获取原始的html marginRight,考虑到可能的非数字值,比如auto
  4305. const originalMarginRight = window.getComputedStyle(document.documentElement).marginRight;
  4306. const marginRightValue = parseFloat(originalMarginRight) || 0; // 将非数字值转换为0
  4307.  
  4308. if (scrollbarWidth > 0) {
  4309. // 保存原始的marginRight,并设置新的值以补偿滚动条宽度
  4310. document.documentElement.style.setProperty('--original-margin-right', originalMarginRight);
  4311. document.documentElement.style.marginRight = `${marginRightValue + scrollbarWidth}px`;
  4312. }
  4313.  
  4314. // 保存原始的overflow样式
  4315. document.documentElement.setAttribute('data-original-overflow', document.documentElement.style.overflow);
  4316. document.documentElement.style.overflow = 'hidden';
  4317. }
  4318.  
  4319. const allowScrollIfNeeded = () => {
  4320. OJBetter.state.openDialogCount--;
  4321. if (OJBetter.state.openDialogCount === 0) {
  4322. // 恢复原始的html marginRight和overflow样式
  4323. const originalMarginRight = document.documentElement.style.getPropertyValue('--original-margin-right');
  4324. document.documentElement.style.marginRight = originalMarginRight;
  4325. document.documentElement.style.removeProperty('--original-margin-right');
  4326.  
  4327. const originalOverflow = document.documentElement.getAttribute('data-original-overflow');
  4328. document.documentElement.style.overflow = originalOverflow;
  4329. document.documentElement.removeAttribute('data-original-overflow');
  4330. }
  4331. };
  4332.  
  4333. dialog.addEventListener('close', allowScrollIfNeeded);
  4334. }
  4335.  
  4336. /**
  4337. * 关闭并移除模态对话框
  4338. * @param {JQuery<HTMLElement>} element
  4339. */
  4340. function OJB_closeAndRemoveModal(element) {
  4341. const dialog = element.get(0);
  4342. dialog.close();
  4343. dialog.remove();
  4344. }
  4345.  
  4346. /**
  4347. * 关闭并移除模态对话框
  4348. * @param {JQuery<HTMLElement>} element
  4349. */
  4350. function OJB_closeModal(element) {
  4351. const dialog = element.get(0);
  4352. dialog.close();
  4353. }
  4354.  
  4355. /**
  4356. * 清除i18next的缓存数据并刷新
  4357. */
  4358. function clearI18nextCache() {
  4359. Object.keys(localStorage)
  4360. .filter(key => key.startsWith('i18next_res_'))
  4361. .forEach(key => localStorage.removeItem(key));
  4362. window.location.reload();
  4363. }
  4364.  
  4365. /**
  4366. * 清除网站本地化数据
  4367. */
  4368. async function clearWebsiteL10nData() {
  4369. OJBetter.common.database.localizeSubsData.clear().then(() => {
  4370. console.log('localizeSubsData table has been cleared');
  4371. window.location.reload();
  4372. }).catch((error) => {
  4373. console.error('Failed to clear localizeSubsData table:', error);
  4374. });
  4375. }
  4376.  
  4377. /**
  4378. * 从Pre代码块中获取原始代码
  4379. * @param {HTMLElement} element pre代码块元素
  4380. * @returns {string|null} 代码文本
  4381. */
  4382. function OJB_getCodeFromPre(element) {
  4383. /**
  4384. * 从Ace格式化的代码块中获取原始代码
  4385. * @param {HTMLElement} element pre代码块元素
  4386. * @returns {string} 代码文本
  4387. */
  4388. const getCodeFromAcePre = function (element) {
  4389. const editor = ace.edit(element);
  4390. return editor.getValue();
  4391. }
  4392.  
  4393. /**
  4394. * 从Pretty格式化的代码块中获取原始代码-1
  4395. * 代码直接存放在 pre 元素中
  4396. * @param {HTMLElement} element pre代码块元素
  4397. * @returns {string} 代码文本
  4398. */
  4399. const getCodeFromPrettyPre = function (element) {
  4400. return Array.from(element.querySelectorAll('li')).map(function (li) {
  4401. return li.textContent;
  4402. }).join('\n');
  4403. }
  4404.  
  4405. /**
  4406. * 从Pretty格式化的代码块中获取原始代码-2
  4407. * 代码存放在子元素 code 中
  4408. * @param {HTMLElement} element pre代码块元素
  4409. * @returns {string} 代码文本
  4410. */
  4411. const getCodeFromPreChild = function (element) {
  4412. const code = element.querySelector("code.prettyprint");
  4413. if (code.classList.contains("linenums")) {
  4414. return getCodeFromPrettyPre(element);
  4415. } else {
  4416. return element.querySelector("code.prettyprint").textContent;
  4417. }
  4418. }
  4419.  
  4420. let result;
  4421. if (element.id === "submission-code") {
  4422. result = getCodeFromAcePre(element);
  4423. } else if (element.classList.contains("prettyprint")) {
  4424. result = getCodeFromPrettyPre(element);
  4425. } else if (element.querySelector("code.prettyprint")) {
  4426. result = getCodeFromPreChild(element);
  4427. } else {
  4428. result = "";
  4429. }
  4430. result = result.replace(/\u00A0/g, ''); // 过滤文本中的U+00a0字符(由&nbsp;造成的)
  4431. return result;
  4432. }
  4433.  
  4434. /**
  4435. * 判断代码的语言
  4436. * @param {string} code 代码文本
  4437. * @returns {string} 可能的语言
  4438. */
  4439. function OJB_codeLangDetect(code) {
  4440. result = hljs.highlightAuto(code);
  4441. return result.language;
  4442. }
  4443.  
  4444. /**
  4445. * 获取指定命名空间下的所有i18n翻译键值对。
  4446. *
  4447. * @param {string} namespace - 要获取键值对的i18next命名空间。
  4448. * @returns {Map<string, string>} 一个包含命名空间下所有键值对的Map对象。
  4449. */
  4450. function OJB_getAllI18nKeysForNamespace(namespace) {
  4451. const language = i18next.language; // 获取当前语言
  4452. const resources = i18next.store.data[language]; // 获取当前语言的所有资源
  4453. const nsResources = resources[namespace]; // 获取特定命名空间的资源
  4454. const resultMap = new Map();
  4455.  
  4456. if (nsResources) {
  4457. // 遍历命名空间下的所有键值对,并添加到Map中
  4458. Object.keys(nsResources).forEach(key => {
  4459. resultMap.set(key, nsResources[key]);
  4460. });
  4461. } else {
  4462. console.log(`No resources found for namespace "${namespace}"`);
  4463. }
  4464.  
  4465. return resultMap;
  4466. }
  4467.  
  4468. /**
  4469. * 更新检查
  4470. */
  4471. async function checkScriptVersion() {
  4472. try {
  4473. const versionResponse = await OJB_GMRequest({
  4474. method: "GET",
  4475. url: "https://aowuucdn.oss-accelerate.aliyuncs.com/script/versions.json",
  4476. timeout: 10 * 1e3,
  4477. nocache: true
  4478. });
  4479. const versionData = JSON.parse(versionResponse.responseText);
  4480. const { [OJBetter.state.formatName]: { dev: version_dev, release: version_release } } = versionData;
  4481. const baseUrls = {
  4482. greasyfork: 'https://update.greasyfork.org/scripts/465777/Codeforces%20Better%21.user.js',
  4483. github: `https://github.com/beijixiaohu/OJBetter/raw/main/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`,
  4484. aliyunoss: `https://aowuucdn.oss-accelerate.aliyuncs.com/script/${OJBetter.about.updateChannel}/${OJBetter.state.formatName}.user.js`
  4485. };
  4486. /** @type {string} 更新跳转url */
  4487. const updateUrl = baseUrls[OJBetter.about.updateSource];
  4488. /** @type {string} 是否暂时跳过cookie */
  4489. const skipUpdate = OJB_getCookie("skipUpdate");
  4490. /** @type {string} 当前更新频道的最新版本 */
  4491. const version = OJBetter.about.updateChannel == "release" ? version_release : version_dev;
  4492. if (OJB_compareVersions(version, OJBetter.state.version) === 1 && skipUpdate !== "true") {
  4493. const updateConfirmed = await OJB_createDialog(
  4494. i18next.t('update.title', { ns: 'dialog', scriptName: OJBetter.state.name }),
  4495. i18next.t('update.content', { ns: 'dialog', oldVersion: OJBetter.state.version, newVersion: version }),
  4496. [
  4497. i18next.t('update.buttons.0', { ns: 'dialog' }),
  4498. i18next.t('update.buttons.1', { ns: 'dialog' })
  4499. ],
  4500. true
  4501. );
  4502.  
  4503. if (updateConfirmed) {
  4504. window.location.href = updateUrl;
  4505. } else {
  4506. document.cookie = "skipUpdate=true; path=/";
  4507. }
  4508. }
  4509. } catch (error) {
  4510. console.error("Update check failed: ", error);
  4511. }
  4512. }
  4513.  
  4514. /**
  4515. * 公告
  4516. */
  4517. async function showAnnounce() {
  4518. /** @type {string} 最新公告版本*/
  4519. const lastAnnounceVer = i18next.t('lastVersion', { ns: 'announce' });
  4520. if (OJB_compareVersions(OJBetter.state.version, OJBetter.state.lastAnnounceVer) === 1) {
  4521. const title = `🎉${i18next.t('announce.title', { ns: 'dialog' })} ${OJBetter.state.version}`;
  4522. /** @type {Boolean} 是否是新的公告 */
  4523. const isNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.lastReadAnnounceVer) === 1;
  4524. /** @type {Boolean} 是否展示新的公告(高于当前版本的测试公告不展示) */
  4525. const showNewAnnounceVer = OJB_compareVersions(lastAnnounceVer, OJBetter.state.version) !== 1;
  4526. /**
  4527. * 获取公告的内容
  4528. * @returns {string} 公告内容
  4529. */
  4530. const getAnnounceContent = function () {
  4531. // 获取公告
  4532. const announceMap = OJB_getAllI18nKeysForNamespace('announce');
  4533. // 移除 'lastVersion' 键
  4534. announceMap.delete('lastVersion');
  4535. // 将 Map 转换为数组并根据版本号排序
  4536. const sortedVersions = [...announceMap.keys()].sort(OJB_compareVersions).reverse();
  4537. let content = "";
  4538. sortedVersions.forEach(version => {
  4539. content += `### ${version}\n\n`; // 使用版本号作为标题
  4540. content += announceMap.get(version); // 添加对应版本的公告内容
  4541. content += "\n\n";
  4542. });
  4543.  
  4544. return content;
  4545. };
  4546.  
  4547. const content = (() => {
  4548. if (isNewAnnounceVer && showNewAnnounceVer) {
  4549. return `${i18next.t('announce.prefix', { ns: 'dialog' })}\n\n${getAnnounceContent()}`;
  4550. } else {
  4551. return i18next.t('announce.divContent', { ns: 'dialog' });
  4552. }
  4553. })();
  4554. const ok = await OJB_createDialog(
  4555. title,
  4556. content,
  4557. [
  4558. null,
  4559. i18next.t('announce.buttons.0', { ns: 'dialog' })
  4560. ],
  4561. true
  4562. ); //跳过折叠块确认
  4563. if (ok) {
  4564. if (isNewAnnounceVer && showNewAnnounceVer) {
  4565. GM_setValue('lastReadAnnounceVer', lastAnnounceVer);
  4566. }
  4567. GM_setValue('lastAnnounceVer', OJBetter.state.version);
  4568. }
  4569. }
  4570. };
  4571.  
  4572. /**
  4573. * 页面顶部提示信息alert类
  4574. */
  4575. class LoadingMessage {
  4576. constructor() {
  4577. this._statusElement = null;
  4578. this._isDisplayed = false;
  4579. this.init();
  4580. }
  4581.  
  4582. /**
  4583. * 初始化加载提示信息
  4584. */
  4585. init() {
  4586. this._statusElement = this.createStatusElement();
  4587. this.insertStatusElement();
  4588. }
  4589.  
  4590. /**
  4591. * 创建提示信息元素
  4592. */
  4593. createStatusElement() {
  4594. const statusElement = $("<div></div>").addClass("alert OJBetter_alert")
  4595. .css({
  4596. "margin": "1em",
  4597. "text-align": "center",
  4598. "position": "relative"
  4599. }).hide();
  4600. return statusElement;
  4601. }
  4602.  
  4603. /**
  4604. * 插入提示信息
  4605. * @returns {void}
  4606. */
  4607. insertStatusElement() {
  4608. (OJBetter.typeOfPage.is_mSite ? $("header") : $(".menu-box:first").next()).after(this._statusElement);
  4609. }
  4610.  
  4611. /**
  4612. * 显示提示信息
  4613. */
  4614. showStatus() {
  4615. this._statusElement.show();
  4616. this._isDisplayed = true;
  4617. }
  4618.  
  4619. /**
  4620. * 隐藏提示信息
  4621. */
  4622. hideStatus() {
  4623. this._statusElement.fadeOut(500);
  4624. this._isDisplayed = false;
  4625. }
  4626.  
  4627. /**
  4628. * 移除提示信息
  4629. */
  4630. removeStatus() {
  4631. this._statusElement.remove();
  4632. this._isDisplayed = false;
  4633. }
  4634.  
  4635. /**
  4636. * 更新提示信息
  4637. * @param {string} text 提示信息文本
  4638. * @param {string} type 提示信息类型,可选值:info, success, warning, error
  4639. * @param {number} timeout 提示信息显示的持续时间(毫秒), 默认为无限长
  4640. */
  4641. updateStatus(text, type = 'info', timeout = Infinity, isMarkdown = false) {
  4642. if (isMarkdown) {
  4643. let md = window.markdownit({
  4644. html: !is_escapeHTML,
  4645. });
  4646. text = md.render(text);
  4647. }
  4648. this._statusElement.html(text).removeClass("alert-info alert-success alert-warning alert-error").addClass(`alert-${type}`);
  4649. if (!this._isDisplayed) {
  4650. this.showStatus();
  4651. }
  4652. if (timeout !== Infinity) {
  4653. setTimeout(() => {
  4654. this.hideStatus();
  4655. }, timeout);
  4656. }
  4657. }
  4658. }
  4659.  
  4660. /**
  4661. * 获取网站本地化的数据
  4662. * @param {*} localizationLanguage 本地化语言
  4663. * @returns {Promise<Object>} 本地化数据
  4664. */
  4665. async function getLocalizeWebsiteJson(localizationLanguage) {
  4666. let data = await OJBetter.common.database.localizeSubsData.get(localizationLanguage);
  4667. let url = localizationLanguage === "zh" ?
  4668. `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/subs/${OJBetter.state.formatName}.json` :
  4669. `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${localizationLanguage}/resources/subs/${OJBetter.state.formatName}.json`;
  4670. if (data) data = data.data;
  4671. if (!data) {
  4672. // 如果本地没有数据,从远端获取并保存
  4673. data = await OJB_getExternalJSON(url);
  4674. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: data });
  4675. } else {
  4676. // 如果本地有数据,检查是否已经在当前会话中尝试过更新
  4677. const sessionKey = `ojb_updateL10nWebsiteJson_${localizationLanguage}`;
  4678. if (!OJB_isSameBrowserSession(sessionKey)) {
  4679. // 如果尚未更新,则在后台更新
  4680. (async () => {
  4681. try {
  4682. const newData = await OJB_getExternalJSON(url);
  4683. await OJBetter.common.database.localizeSubsData.put({ lang: localizationLanguage, data: newData });
  4684. console.log("Website local data has been refreshed!");
  4685. } catch (error) {
  4686. console.error('Failed to update localization data:', error);
  4687. }
  4688. })();
  4689. }
  4690. }
  4691. return data;
  4692. }
  4693.  
  4694. /**
  4695. * 网站本地化替换
  4696. * @returns
  4697. */
  4698. async function localizeWebsite() {
  4699. if (OJBetter.localization.websiteLang === "initial") return;
  4700.  
  4701. // 设置网页语言
  4702. var htmlTag = document.getElementsByTagName("html")[0];
  4703. htmlTag.setAttribute("lang", OJBetter.localization.websiteLang);
  4704.  
  4705. // 获取网站本地化的数据
  4706. var subs = await getLocalizeWebsiteJson(OJBetter.localization.websiteLang);
  4707.  
  4708. /**
  4709. * 文本节点遍历替换
  4710. * @param {JQuery} $nodes jQuery对象
  4711. * @param {Object} textReplaceRules 文本替换规则对象
  4712. * @param {string} key 应用的规则集的名字
  4713. */
  4714. const traverseTextNodes = ($nodes, textReplaceRules, key) => {
  4715. if (!$nodes) return;
  4716.  
  4717. $nodes.each((_, node) => {
  4718. if (node.nodeType === Node.TEXT_NODE) {
  4719. Object.entries(textReplaceRules).forEach(([match, replace]) => {
  4720. try {
  4721. const regex = new RegExp(match, 'g');
  4722. const beforeText = node.textContent;
  4723. node.textContent = node.textContent.replace(regex, replace);
  4724. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4725. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4726. }
  4727. } catch (error) {
  4728. console.error(`Error processing text replacement for match: ${match}`, error);
  4729. }
  4730. });
  4731. } else {
  4732. $(node).contents().each((_, childNode) => {
  4733. traverseTextNodes($(childNode), textReplaceRules, key);
  4734. });
  4735. }
  4736. });
  4737. };
  4738.  
  4739. /**
  4740. * value替换
  4741. * @param {JQuery} $nodes jQuery对象
  4742. * @param {Object} valueReplaceRules 值替换规则对象
  4743. * @param {string} key 应用的规则集的名字
  4744. */
  4745. function traverseValueNodes($nodes, valueReplaceRules, key) {
  4746. if (!$nodes) return;
  4747.  
  4748. $nodes.each(function () {
  4749. let $node = $(this);
  4750. if ($node.is('[value]')) {
  4751. Object.keys(valueReplaceRules).forEach(match => {
  4752. const replace = valueReplaceRules[match];
  4753. const regex = new RegExp(match, 'g');
  4754. let currentValue = $node.val();
  4755. let newValue = currentValue.replace(regex, replace);
  4756. $node.val(newValue);
  4757. if (OJBetter.dev.isRuleMarkingEnabled) {
  4758. if (newValue !== currentValue) $($node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4759. }
  4760. });
  4761. } else {
  4762. $node.children().each(function () {
  4763. traverseValueNodes($(this), valueReplaceRules, key);
  4764. });
  4765. }
  4766. });
  4767. }
  4768.  
  4769. /**
  4770. * 严格的文本节点遍历替换
  4771. * 要求被替换文本严格与规则文本一致
  4772. * @param {JQuery} $nodes jQuery对象
  4773. * @param {Object} textReplaceRules 文本替换规则对象
  4774. * @param {string} key 应用的规则集的名字
  4775. */
  4776. const strictTraverseTextNodes = ($nodes, textReplaceRules, key) => {
  4777. if (!$nodes) return;
  4778.  
  4779. $nodes.each((_, node) => {
  4780. if (node.nodeType === Node.TEXT_NODE) {
  4781. const trimmedNodeText = node.textContent.trim();
  4782. for (const [match, replacement] of Object.entries(textReplaceRules)) {
  4783. if (trimmedNodeText === match) {
  4784. const beforeText = node.textContent;
  4785. node.textContent = replacement;
  4786. if (node.textContent !== beforeText && OJBetter.dev.isRuleMarkingEnabled) {
  4787. $(node).after(`<span class="markingTextReplaceRule">${key}</span>`);
  4788. }
  4789. }
  4790. }
  4791. } else {
  4792. $(node).contents().each((_, childNode) => {
  4793. strictTraverseTextNodes($(childNode), textReplaceRules, key);
  4794. });
  4795. }
  4796. });
  4797. };
  4798.  
  4799. /**
  4800. * 应用文本替换
  4801. */
  4802. let commonReplacements = subs.commonReplacements;
  4803. Object.entries(commonReplacements).forEach(([key, value]) => {
  4804. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4805. classSelectors.forEach(classSelector => {
  4806. if (value.isStrict) {
  4807. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4808. } else {
  4809. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4810. }
  4811. });
  4812. });
  4813.  
  4814. /**
  4815. * 应用value替换
  4816. */
  4817. let InputValueReplacements = subs.InputValueReplacements;
  4818. Object.entries(InputValueReplacements).forEach(([key, value]) => {
  4819. const classSelectors = Array.isArray(value.class) ? value.class : [value.class];
  4820. classSelectors.forEach(classSelector => {
  4821. traverseValueNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4822. });
  4823. });
  4824.  
  4825. /**
  4826. * 动态添加的文本的替换
  4827. */
  4828. let dynamicReplacements = subs.dynamicReplacements;
  4829. Object.entries(dynamicReplacements).forEach(([key, value]) => {
  4830. const classSelectors = Array.isArray(value.class) ? value.class : [value.class]; // 兼容,class的值可以为数组或者字符串
  4831. classSelectors.forEach(classSelector => {
  4832. OJB_observeElement({
  4833. selector: classSelector,
  4834. callback: (node) => {
  4835. // let popupContent = node.textContent.replace(/^×/, ''); // 去除开头多余的 '×' 字符
  4836. if (value.isStrict) {
  4837. strictTraverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4838. } else {
  4839. traverseTextNodes(OJB_safeCreateJQElement(`${classSelector}`), value.rules, key);
  4840. }
  4841. }
  4842. });
  4843. });
  4844. });
  4845.  
  4846. // 杂项
  4847. (function () {
  4848. // 选项汉化input[type="radio"]
  4849. var translations = {
  4850. "as individual participant": "个人",
  4851. "as a team member": "作为一个团队成员",
  4852. };
  4853. $('input[type="radio"]').each(function () {
  4854. var tag = $(this).parent().contents().filter(function () {
  4855. return this.nodeType === Node.TEXT_NODE;
  4856. });
  4857. for (var i = 0; i < tag.length; i++) {
  4858. var text = tag[i].textContent.trim();
  4859. if (translations.hasOwnProperty(text)) {
  4860. $(this).addClass(text);
  4861. tag[i].replaceWith(translations[text]);
  4862. break;
  4863. }
  4864. }
  4865. });
  4866. })();
  4867. (function () {
  4868. var translations = {
  4869. "(standard input\/output)": "标准输入/输出",
  4870. };
  4871. $("div.notice").each(function () {
  4872. var tag = $(this).children().eq(0).text();
  4873. for (var property in translations) {
  4874. if (tag.match(property)) {
  4875. $(this).children().eq(0).text(translations[property]);
  4876. break;
  4877. }
  4878. }
  4879. });
  4880. })();
  4881.  
  4882. // 轻量站特殊
  4883. if (OJBetter.typeOfPage.is_mSite) {
  4884. traverseTextNodes($('nav'), commonReplacements['.second-level-menu']['rules']);
  4885. }
  4886. if (OJBetter.typeOfPage.is_mSite) {
  4887. (function () {
  4888. var translations = {
  4889. "Announcements": "公告",
  4890. "Submissions": "提交记录",
  4891. "Contests": "比赛",
  4892. };
  4893. $(".caption").each(function () {
  4894. var optionValue = $(this).text();
  4895. if (translations[optionValue]) {
  4896. $(this).text(translations[optionValue]);
  4897. }
  4898. });
  4899. })();
  4900. }
  4901. };
  4902.  
  4903. /**
  4904. * i18next初始化
  4905. */
  4906. async function initI18next() {
  4907. return new Promise((resolve, reject) => {
  4908. i18next
  4909. .use(i18nextChainedBackend)
  4910. .init({
  4911. lng: OJBetter.localization.scriptLang,
  4912. ns: ['common', 'settings', 'config', 'dialog', 'alert', 'translator',
  4913. 'button', 'codeEditor', 'comments', 'announce', 'logMessage'], // 命名空间列表
  4914. defaultNS: 'settings',
  4915. fallbackLng: ['zh', OJBetter.translation.targetLang],
  4916. load: 'currentOnly',
  4917. debug: false,
  4918. backend: {
  4919. backends: [
  4920. i18nextLocalStorageBackend,
  4921. i18nextHttpBackend
  4922. ],
  4923. backendOptions: [{
  4924. prefix: 'i18next_res_',
  4925. expirationTime: 7 * 24 * 60 * 60 * 1000,
  4926. defaultVersion: `v${OJBetter.state.version}`,
  4927. store: typeof window !== 'undefined' ? window.localStorage : null
  4928. }, {
  4929. /* options for secondary backend */
  4930. loadPath: (lng, ns) => {
  4931. if (lng[0] === 'zh' || lng[0] === 'zh-Hans') {
  4932. return `https://aowuucdn.oss-accelerate.aliyuncs.com/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4933. }
  4934. return `https://aowuucdn.oss-accelerate.aliyuncs.com/i18n/${lng}/resources/locales/${OJBetter.state.formatName}/${ns}.json`;
  4935. }
  4936. }]
  4937. }
  4938. }, (err, t) => {
  4939. if (err) {
  4940. reject(err);
  4941. } else {
  4942. jqueryI18next.init(i18next, $, {
  4943. useOptionsAttr: true
  4944. });
  4945. resolve(t);
  4946. }
  4947. });
  4948. });
  4949. };
  4950.  
  4951. /**
  4952. * 抽象命令类
  4953. */
  4954. class Command {
  4955. execute() { }
  4956. undo() { }
  4957. }
  4958.  
  4959. /**
  4960. * 命令调用者
  4961. */
  4962. class CommandInvoker {
  4963. constructor() {
  4964. this.history = [];
  4965. }
  4966.  
  4967. /**
  4968. * 执行命令
  4969. * @param {Command} command 命令对象
  4970. */
  4971. execute(command) {
  4972. this.history.push(command);
  4973. command.execute();
  4974. }
  4975.  
  4976. /**
  4977. * 撤销命令
  4978. */
  4979. undo() {
  4980. const command = this.history.pop();
  4981. if (command) {
  4982. command.undo();
  4983. }
  4984. }
  4985. }
  4986.  
  4987. /**
  4988. * 接收者
  4989. */
  4990. class DOMContainer {
  4991. /**
  4992. * @param {JQueryObject} element 容器对象
  4993. */
  4994. constructor(element) {
  4995. this.containerElement = element;
  4996. }
  4997.  
  4998. /**
  4999. * 添加元素
  5000. * @param {JQueryObject} element 元素对象
  5001. * @returns {JQueryObject} 添加的元素对象
  5002. */
  5003. add(element) {
  5004. this.containerElement.append(element);
  5005. return this.containerElement.children().last();
  5006. }
  5007.  
  5008. /**
  5009. * 删除元素
  5010. * @param {JQueryObject} element 元素对象
  5011. */
  5012. remove(element) {
  5013. $(element).remove();
  5014. }
  5015. }
  5016.  
  5017. /**
  5018. * 具体命令类:添加元素
  5019. */
  5020. class AddElementCommand extends Command {
  5021. /**
  5022. * @param {DOMContainer} receiver 接收者
  5023. * @param {JQueryObject} element 元素对象
  5024. */
  5025. constructor(receiver, element) {
  5026. super();
  5027. this.receiver = receiver;
  5028. this.element = element;
  5029. this.addedElement = null;
  5030. }
  5031.  
  5032. execute() {
  5033. this.addedElement = this.receiver.add(this.element);
  5034. }
  5035.  
  5036. undo() {
  5037. if (this.addedElement) {
  5038. this.receiver.remove(this.addedElement);
  5039. }
  5040. }
  5041. }
  5042.  
  5043. /**
  5044. * 具体命令类:删除元素
  5045. */
  5046. class RemoveElementCommand extends Command {
  5047. /**
  5048. * @param {DOMContainer} receiver 接收者
  5049. * @param {JQueryObject} element 元素对象
  5050. */
  5051. constructor(receiver, element) {
  5052. super();
  5053. this.receiver = receiver;
  5054. this.element = element;
  5055. this.parent = $(element).parent();
  5056. this.nextSibling = $(element).next();
  5057. }
  5058.  
  5059. execute() {
  5060. this.receiver.remove(this.element);
  5061. }
  5062.  
  5063. undo() {
  5064. if (this.nextSibling.length > 0) {
  5065. $(this.element).insertBefore(this.nextSibling);
  5066. } else {
  5067. this.parent.append(this.element);
  5068. }
  5069. }
  5070. }
  5071.  
  5072. /**
  5073. * 验证器
  5074. */
  5075. class Validator {
  5076. /**
  5077. * 表单必填项空值校验
  5078. */
  5079. static required(structure) {
  5080. let config = {};
  5081. let allFieldsValid = true;
  5082. for (const key in structure) {
  5083. let value = key.type == 'checkbox' ?
  5084. $(key).prop("checked") : $(key).val();
  5085.  
  5086. config[structure[key].value] = value;
  5087.  
  5088. if (value || structure[key].require === false) {
  5089. $(key).removeClass('is_null');
  5090. } else {
  5091. $(key).addClass('is_null');
  5092. allFieldsValid = false;
  5093. }
  5094. }
  5095. return {
  5096. valid: allFieldsValid,
  5097. config: config
  5098. };
  5099. }
  5100.  
  5101. /**
  5102. * 表单合法性校验
  5103. */
  5104. static checkKeyValuePairs(structure, config) {
  5105. let errorKeys = [];
  5106. let allFieldsValid = true;
  5107.  
  5108. for (const key in structure) {
  5109. const { check, value } = structure[key];
  5110. const fieldValue = config[value];
  5111.  
  5112. // 如果字段没有值或校验类型不匹配,则跳过当前迭代
  5113. if (!fieldValue) continue;
  5114.  
  5115. let isValid = true;
  5116. switch (check) {
  5117. case 'keyValuePairs':
  5118. isValid = Validator.keyValuePairs(fieldValue);
  5119. break;
  5120. case 'dotSeparatedPath':
  5121. isValid = Validator.validateDotSeparatedPath(fieldValue);
  5122. break;
  5123. default:
  5124. // 没有匹配的校验类型
  5125. continue;
  5126. }
  5127.  
  5128. Validator.toggleErrorDisplay(key, isValid);
  5129. if (!isValid) {
  5130. allFieldsValid = false;
  5131. errorKeys.push(key);
  5132. }
  5133. }
  5134.  
  5135. return {
  5136. valid: allFieldsValid,
  5137. errorKeys: errorKeys
  5138. };
  5139. }
  5140.  
  5141. /**
  5142. * 切换错误信息的显示和隐藏
  5143. * @param {string} key - 字段的键
  5144. * @param {boolean} isValid - 字段值是否有效
  5145. */
  5146. static toggleErrorDisplay(key, isValid) {
  5147. const errorMessage = i18next.t('common.unValid', { ns: 'settings' });
  5148. const $errorSpan = $(key).prev('span.text-error');
  5149. if (!isValid) {
  5150. if (!$errorSpan.length) {
  5151. $(key).before(`<span class="text-error" style="color: red;">${errorMessage}</span>`);
  5152. }
  5153. } else {
  5154. $errorSpan.remove();
  5155. }
  5156. }
  5157.  
  5158. /**
  5159. * 键值对合法性校验
  5160. * @param {string} value
  5161. * @returns {boolean}
  5162. */
  5163. static keyValuePairs(value) {
  5164. const keyValuePairs = value.split('\n');
  5165. // 允许值中包含空格和冒号
  5166. const regex = /^[a-zA-Z0-9_-]+\s*:\s*.+$/;
  5167. return keyValuePairs.every(pair => regex.test(pair));
  5168. }
  5169.  
  5170.  
  5171. /**
  5172. * 点分隔符路径格式校验,允许加减运算
  5173. * @param {string} path
  5174. * @returns {boolean}
  5175. */
  5176. static validateDotSeparatedPath(path) {
  5177. // 正则表达式允许标识符之间有点号,标识符可以包含加减运算
  5178. const regex = /^([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*\.)*([a-zA-Z0-9_-]+(\s*[\+\-]\s*[a-zA-Z0-9_-]+)*)$/;
  5179. return regex.test(path);
  5180. }
  5181. }
  5182.  
  5183. /**
  5184. * 配置管理
  5185. */
  5186. class ConfigManager {
  5187. /**
  5188. * @param {HTMLElement} element - 挂载容器
  5189. * @param {string} prefix - 前缀
  5190. * @param {object} tempConfig - 配置内容
  5191. * @param {object} structure - 配置结构
  5192. * @param {object} configHTML - 配置编辑页面HTML
  5193. * @param {boolean} allowChoice - 是否允许选择列表项
  5194. */
  5195. constructor(element, prefix, tempConfig, structure, configHTML, allowChoice = true) {
  5196. /** @param 设置面板DIV */
  5197. this.settingMenuDiv = $('#OJBetter_setting_menu');
  5198. this.element = $(element);
  5199. this.prefix = prefix;
  5200. this.tempConfig = tempConfig;
  5201. this.structure = structure;
  5202. this.configHTML = configHTML;
  5203. this.allowChoice = allowChoice;
  5204.  
  5205. this.controlTip = null;
  5206. this.config_bar_list = null;
  5207. this.config_bar_ul = null;
  5208. this.config_add_button = null;
  5209. this.menu = null;
  5210. this.editItem = null;
  5211. this.deleteItem = null;
  5212.  
  5213. // 绑定方法
  5214. this.onAdd = this.onAdd.bind(this);
  5215. this.onEdit = this.onEdit.bind(this);
  5216. this.onDelete = this.onDelete.bind(this);
  5217. this.createListItemElement = this.createListItemElement.bind(this);
  5218.  
  5219. this.lastItemId = 0; // 列表中当前最后一个元素的id号
  5220. this.init();
  5221. }
  5222.  
  5223. init() {
  5224. this.createControlBar();
  5225. this.createContextMenu();
  5226. this.renderList();
  5227. }
  5228.  
  5229. /**
  5230. * 创建控制栏
  5231. */
  5232. createControlBar() {
  5233. this.controlTip = OJB_safeCreateJQElement(`<div id='${this.prefix}configControlTip' style='color:red;'></div>`);
  5234. this.config_bar_list = OJB_safeCreateJQElement(`<div class='config_bar_list' id='${this.prefix}config_bar_list'></div>`);
  5235. this.config_bar_ul = OJB_safeCreateJQElement(`<ul class='config_bar_ul' id='${this.prefix}config_bar_ul'></ul>`);
  5236. this.element.append(this.controlTip);
  5237. this.element.append(this.config_bar_list);
  5238. this.config_bar_list.append(this.config_bar_ul);
  5239. }
  5240.  
  5241. /**
  5242. * 创建右键菜单
  5243. */
  5244. createContextMenu() {
  5245. const menu = OJB_safeCreateJQElement(`<div id='config_bar_menu' style='display: none;'></div>`);
  5246. const editItem = OJB_safeCreateJQElement(`
  5247. <div class='config_bar_menu_item' id='config_bar_menu_edit'>
  5248. ${i18next.t('contextMenu.edit', { ns: 'translator' })}
  5249. </div>`);
  5250. const deleteItem = OJB_safeCreateJQElement(`
  5251. <div class='config_bar_menu_item' id='config_bar_menu_delete'>
  5252. ${i18next.t('contextMenu.delete', { ns: 'translator' })}
  5253. </div>`);
  5254. menu.append(editItem);
  5255. menu.append(deleteItem);
  5256. this.editItem = editItem;
  5257. this.deleteItem = deleteItem;
  5258. this.menu = menu;
  5259. this.settingMenuDiv.append(menu);
  5260. }
  5261.  
  5262. /**
  5263. * 关闭右键菜单
  5264. */
  5265. closeContextMenu() {
  5266. this.menu.css({ display: "none" });
  5267. }
  5268.  
  5269. /**
  5270. * 创建列表项
  5271. * @param {string} text - 列表项文本
  5272. * @returns {HTMLElement} - 列表项
  5273. */
  5274. createListItemElement(text) {
  5275. const id = OJB_getRandomNumber(4);
  5276. const li = $("<li></li>");
  5277. const radio = OJB_safeCreateJQElement(`<input type='radio' name='${this.prefix}config_item'></input>`)
  5278. .attr("value", text)
  5279. .attr("id", id)
  5280. .attr("prev_id", this.lastItemId)
  5281. .appendTo(li);
  5282. if (!this.allowChoice) {
  5283. radio.prop("disabled", true);
  5284. }
  5285. const label = OJB_safeCreateJQElement(`<label for='${id}' class='config_bar_ul_li_text'>${text}</label>`).appendTo(li);
  5286.  
  5287.  
  5288. this.lastItemId = id;
  5289.  
  5290. // 添加右键菜单
  5291. li.on("contextmenu", (event) => {
  5292. event.preventDefault();
  5293. this.menu.css({
  5294. display: "block",
  5295. left: event.pageX, top: event.pageY
  5296. });
  5297.  
  5298. const deleteItem = this.deleteItem;
  5299. const editItem = this.editItem;
  5300.  
  5301. // 移除旧事件
  5302. deleteItem.off("click");
  5303. editItem.off("click");
  5304.  
  5305. // 获取 li 在 ul 中的索引
  5306. const index = li.index();
  5307.  
  5308. deleteItem.on("click", () => this.onDelete(index, li));
  5309. editItem.on("click", () => this.onEdit(index, li));
  5310.  
  5311. $(document).one("click", (event) => {
  5312. if (!this.menu.get(0).contains(event.target)) {
  5313. this.closeContextMenu();
  5314. deleteItem.off("click", () => this.onDelete);
  5315. editItem.off("click", () => this.onEdit);
  5316. }
  5317. });
  5318. });
  5319.  
  5320. return li;
  5321. }
  5322.  
  5323. /**
  5324. * 渲染配置列表
  5325. */
  5326. renderList() {
  5327. const list = this.config_bar_ul;
  5328. list.empty(); // 清空
  5329. this.tempConfig.configurations.forEach((item) => {
  5330. list.append(this.createListItemElement(item['name']));
  5331. });
  5332.  
  5333. // 添加按钮
  5334. let addButton = OJB_safeCreateJQElement(`<li id='${this.prefix}add_button' class="tempConfig_add_button">
  5335. <span>+ ${i18next.t('add', { ns: 'common' })}</span>
  5336. </li>`);
  5337. this.config_add_button = addButton;
  5338. list.append(addButton);
  5339. addButton.on("click", this.onAdd);
  5340. }
  5341.  
  5342. /**
  5343. * 添加配置项
  5344. */
  5345. onAdd() {
  5346. const configMenu = this.createConfigHTML();
  5347. const structure = this.structure;
  5348.  
  5349. configMenu.on("click", "#tempConfig_save", () => {
  5350.  
  5351. // 检查必填字段
  5352. const { valid, config } = Validator.required(structure);
  5353. if (!valid) return;
  5354.  
  5355. // 检查键值对
  5356. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5357. if (!checkOk) return;
  5358.  
  5359. this.tempConfig.configurations.push(config);
  5360.  
  5361. this.createListItemElement(config.name).insertBefore(this.config_add_button);
  5362.  
  5363. configMenu.remove();
  5364. });
  5365.  
  5366. configMenu.on("click", ".btn-close", () => {
  5367. configMenu.remove();
  5368. });
  5369. }
  5370.  
  5371. /**
  5372. * 修改配置项
  5373. * @param {number} index - 配置项索引
  5374. * @param {HTMLElement} li - 配置项
  5375. * @returns {void}
  5376. */
  5377. onEdit(index, li) {
  5378. const configMenu = this.createConfigHTML();
  5379. const structure = this.structure;
  5380.  
  5381. this.closeContextMenu();
  5382.  
  5383. // 填充表单
  5384. for (const [key, { value, type }] of Object.entries(this.structure)) {
  5385. const configValue = this.tempConfig.configurations[index][value];
  5386. const $element = $(key);
  5387. if (type === 'checkbox') {
  5388. $element.prop("checked", configValue);
  5389. } else {
  5390. $element.val(configValue);
  5391. }
  5392. }
  5393.  
  5394. configMenu.on("click", "#tempConfig_save", () => {
  5395. // 检查必填字段
  5396. const { valid, config } = Validator.required(structure);
  5397. if (!valid) return;
  5398.  
  5399. // 检查键值对
  5400. const { valid: checkOk, errorKey } = Validator.checkKeyValuePairs(structure, config);
  5401. if (!checkOk) return;
  5402.  
  5403. // 更新配置
  5404. this.tempConfig.configurations[index] = config;
  5405. li.find('label').text(config.name);
  5406.  
  5407. OJB_closeAndRemoveModal(configMenu);
  5408. });
  5409.  
  5410. configMenu.on("click", ".btn-close", () => {
  5411. OJB_closeAndRemoveModal(configMenu);
  5412. });
  5413. }
  5414.  
  5415. /**
  5416. * 删除配置项
  5417. * @param {number} index - 配置项索引
  5418. * @param {HTMLElement} li - 配置项
  5419. * @returns {void}
  5420. */
  5421. onDelete(index, li) {
  5422. this.closeContextMenu();
  5423. this.tempConfig.configurations.splice(index, 1);
  5424. li.remove();
  5425. }
  5426.  
  5427. /**
  5428. * 创建配置编辑页面
  5429. * @returns {JQuery<HTMLElement>} 返回配置编辑页面
  5430. */
  5431. createConfigHTML() {
  5432. const configMenu = OJB_safeCreateJQElement(this.configHTML);
  5433. this.settingMenuDiv.after(configMenu);
  5434. OJB_showModal(configMenu);
  5435. OJB_addDraggable(configMenu);
  5436. elementLocalize(configMenu);
  5437. return configMenu;
  5438. }
  5439.  
  5440. /**
  5441. * 获取配置内容
  5442. * @returns {object} - 配置内容
  5443. */
  5444. getTempConfig() {
  5445. return this.tempConfig;
  5446. }
  5447.  
  5448. /**
  5449. * 注册列表项选中改变监听
  5450. */
  5451. registerChoiceChange() {
  5452. this.config_bar_ul.on("change", "input[type='radio']", (event) => {
  5453. const value = event.target.value;
  5454. this.tempConfig.choice = value;
  5455. });
  5456. }
  5457. }
  5458.  
  5459. const OJBetter_setting_sidebar_HTML = `
  5460. <div class="OJBetter_setting_sidebar">
  5461. <ul>
  5462. <li><a href="#basic-settings" id="sidebar-basic-settings" class="active" data-i18n="settings:sidebar.basic"></a></li>
  5463. <li><a href="#l10n_settings" id="sidebar-l10n_settings" data-i18n="settings:sidebar.localization"></a></li>
  5464. <li><a href="#translation-settings" id="sidebar-translation-settings" data-i18n="settings:sidebar.translation"></a></li>
  5465. <li><a href="#clist_rating-settings" id="sidebar-clist_rating-settings" data-i18n="settings:sidebar.clist"></a></li>
  5466. <li><a href="#code_editor-settings" id="sidebar-code_editor-settings" data-i18n="settings:sidebar.monaco"></a></li>
  5467. <li><a href="#preference-settings" id="sidebar-preference-settings" data-i18n="settings:sidebar.preference"></a></li>
  5468. <li><a href="#dev-settings" id="sidebar-dev-settings" data-i18n="settings:sidebar.dev"></a></li>
  5469. <li><a href="#about-settings" id="sidebar-about-settings" data-i18n="settings:sidebar.about"></a></li>
  5470. </ul>
  5471. </div>
  5472. `;
  5473.  
  5474. const basic_settings_HTML = `
  5475. <div id="basic-settings" class="settings-page active">
  5476. <h3 data-i18n="settings:basic.title"></h3>
  5477. <hr>
  5478. <div class='OJBetter_setting_list' style="padding: 0px 10px;">
  5479. <span id="darkMode_span" data-i18n="settings:basic.darkMode.name"></span>
  5480. <div class="dark-mode-selection">
  5481. <label>
  5482. <input class="radio-input" type="radio" name="darkMode" value="dark" />
  5483. <span class="OJBetter_setting_menu_label_text"
  5484. data-i18n="settings:basic.darkMode.options.dark"></span>
  5485. <span class="radio-icon"> </span>
  5486. </label>
  5487. <label>
  5488. <input checked="" class="radio-input" type="radio" name="darkMode" value="light" />
  5489. <span class="OJBetter_setting_menu_label_text"
  5490. data-i18n="settings:basic.darkMode.options.light"></span>
  5491. <span class="radio-icon"> </span>
  5492. </label>
  5493. <label>
  5494. <input class="radio-input" type="radio" name="darkMode" value="follow" />
  5495. <span class="OJBetter_setting_menu_label_text"
  5496. data-i18n="settings:basic.darkMode.options.system"></span>
  5497. <span class="radio-icon"> </span>
  5498. </label>
  5499. </div>
  5500. </div>
  5501. <div class='OJBetter_setting_list'>
  5502. <label for="expandFoldingblocks" data-i18n="settings:basic.expandBlocks"></label>
  5503. <input type="checkbox" id="expandFoldingblocks" name="expandFoldingblocks">
  5504. </div>
  5505. <div class='OJBetter_setting_list'>
  5506. <label for="renderPerfOpt" data-i18n="settings:basic.renderOptimization.label"></label>
  5507. <div class="help_tip">
  5508. ${helpCircleHTML}
  5509. <div class="tip_text" data-i18n="[html]settings:basic.renderOptimization.helpText"></div>
  5510. </div>
  5511. <input type="checkbox" id="renderPerfOpt" name="renderPerfOpt">
  5512. </div>
  5513. <div class='OJBetter_setting_list'>
  5514. <label for="selectElementPerfOpt" data-i18n="settings:basic.selectElementOptimization.label"></label>
  5515. <div class="help_tip">
  5516. ${helpCircleHTML}
  5517. <div class="tip_text" data-i18n="[html]settings:basic.selectElementOptimization.helpText"></div>
  5518. </div>
  5519. <input type="checkbox" id="selectElementPerfOpt" name="selectElementPerfOpt">
  5520. </div>
  5521. <div class='OJBetter_setting_list'>
  5522. <label for="commentPaging" data-i18n="settings:basic.paging.label"></label>
  5523. <div class="help_tip">
  5524. ${helpCircleHTML}
  5525. <div class="tip_text" data-i18n="[html]settings:basic.paging.helpText"></div>
  5526. </div>
  5527. <input type="checkbox" id="commentPaging" name="commentPaging">
  5528. </div>
  5529. <div class='OJBetter_setting_list'>
  5530. <label for="showJumpToLuogu" data-i18n="settings:basic.luoguJump.label"></label>
  5531. <div class="help_tip">
  5532. ${helpCircleHTML}
  5533. <div class="tip_text" data-i18n="[html]settings:basic.luoguJump.helpText"></div>
  5534. </div>
  5535. <input type="checkbox" id="showJumpToLuogu" name="showJumpToLuogu">
  5536. </div>
  5537. <div class='OJBetter_setting_list'>
  5538. <label for="showCF2vjudge" data-i18n="settings:basic.vjudgeJump.label"></label>
  5539. <div class="help_tip">
  5540. ${helpCircleHTML}
  5541. <div class="tip_text" data-i18n="[html]settings:basic.vjudgeJump.helpText"></div>
  5542. </div>
  5543. <input type="checkbox" id="showCF2vjudge" name="showCF2vjudge">
  5544. </div>
  5545. <div class='OJBetter_setting_list'>
  5546. <label for="standingsRecolor" data-i18n="settings:basic.recolor.label"></label>
  5547. <div class="help_tip">
  5548. ${helpCircleHTML}
  5549. <div class="tip_text" data-i18n="[html]settings:basic.recolor.helpText"></div>
  5550. </div>
  5551. <input type="checkbox" id="standingsRecolor" name="standingsRecolor">
  5552. </div>
  5553. <div class='OJBetter_setting_list'>
  5554. <label for="hiddenProblemTag" data-i18n="settings:basic.hiddenProblemTag.label"></label>
  5555. <div class="help_tip">
  5556. ${helpCircleHTML}
  5557. <div class="tip_text" data-i18n="[html]settings:basic.hiddenProblemTag.helpText"></div>
  5558. </div>
  5559. <input type="checkbox" id="hiddenProblemTag" name="hiddenProblemTag">
  5560. </div>
  5561. </div>
  5562. `;
  5563.  
  5564. const l10n_settings_HTML = `
  5565. <div id="l10n_settings" class="settings-page">
  5566. <h3 data-i18n="settings:localization.title"></h3>
  5567. <hr>
  5568. <div class='OJBetter_setting_list'>
  5569. <label for="scriptL10nLanguage" style="display: flex;" data-i18n="settings:localization.scriptLanguageLabel"></label>
  5570. <select id="scriptL10nLanguage" name="scriptL10nLanguage">
  5571. <option value="zh">简体中文</option>
  5572. <option value="zh-Hant">繁體中文</option>
  5573. <option value="en">English</option>
  5574. <option value="de">Deutsch</option>
  5575. <option value="fr">Français</option>
  5576. <option value="ko">한국어</option>
  5577. <option value="pt">Português</option>
  5578. <option value="ja">日本語</option>
  5579. <option value="es">Español</option>
  5580. <option value="it">Italiano</option>
  5581. <option value="hi">हिन्दी</option>
  5582. </select>
  5583. </div>
  5584. <div class='OJBetter_setting_list'>
  5585. <label for="localizationLanguage" style="display: flex;" data-i18n="settings:localization.websiteLanguageLabel"></label>
  5586. <select id="localizationLanguage" name="localizationLanguage">
  5587. <option value="initial">——</option>
  5588. <option value="zh">简体中文</option>
  5589. <option value="zh-Hant">繁體中文</option>
  5590. <option value="de">Deutsch</option>
  5591. <option value="fr">Français</option>
  5592. <option value="ko">한국어</option>
  5593. <option value="pt">Português</option>
  5594. <option value="ja">日本語</option>
  5595. <option value="es">Español</option>
  5596. <option value="it">Italiano</option>
  5597. <option value="hi">हिन्दी</option>
  5598. </select>
  5599. </div>
  5600. <div class='OJBetter_setting_list alert_tip'>
  5601. <div data-i18n="[html]settings:localization.notice.1"></div>
  5602. </div>
  5603. <div class='OJBetter_setting_list alert_tip'>
  5604. <div data-i18n="[html]settings:localization.notice.2"></div>
  5605. </div>
  5606. </div>
  5607. `;
  5608.  
  5609. const translation_settings_HTML = `
  5610. <div id="translation-settings" class="settings-page">
  5611. <h3 data-i18n="settings:translation.title"></h3>
  5612. <hr>
  5613. <h4 data-i18n="settings:translation.options.title"></h4>
  5614. <div class='OJBetter_setting_list'>
  5615. <label for="transTargetLang" style="display: flex;" data-i18n="settings:translation.preference.target.title"></label>
  5616. <div class="help_tip">
  5617. ${helpCircleHTML}
  5618. <div class="tip_text" data-i18n="[html]settings:translation.preference.target.helpText"></div>
  5619. </div>
  5620. <select id="transTargetLang" name="transTargetLang">
  5621. <option value="zh">简体中文</option>
  5622. <option value="zh-Hant">繁體中文</option>
  5623. <option value="de">Deutsch</option>
  5624. <option value="fr">Français</option>
  5625. <option value="ko">한국어</option>
  5626. <option value="pt">Português</option>
  5627. <option value="ja">日本語</option>
  5628. <option value="es">Español</option>
  5629. <option value="it">Italiano</option>
  5630. <option value="hi">हिन्दी</option>
  5631. </select>
  5632. </div>
  5633. <div id="translationServices">
  5634. <label>
  5635. <input type='radio' name='translation' value='deepl'>
  5636. <span class='OJBetter_setting_menu_label_text'
  5637. data-i18n="settings:translation.options.services.deepl"></span>
  5638. </label>
  5639. <label>
  5640. <input type='radio' name='translation' value='iflyrec'>
  5641. <span class='OJBetter_setting_menu_label_text'
  5642. data-i18n="settings:translation.options.services.iflyrec"></span>
  5643. </label>
  5644. <label>
  5645. <input type='radio' name='translation' value='youdao'>
  5646. <span class='OJBetter_setting_menu_label_text'
  5647. data-i18n="settings:translation.options.services.youdao"></span>
  5648. </label>
  5649. <label>
  5650. <input type='radio' name='translation' value='google'>
  5651. <span class='OJBetter_setting_menu_label_text'
  5652. data-i18n="settings:translation.options.services.google"></span>
  5653. </label>
  5654. <label>
  5655. <input type='radio' name='translation' value='caiyun'>
  5656. <span class='OJBetter_setting_menu_label_text'
  5657. data-i18n="settings:translation.options.services.caiyun"></span>
  5658. </label>
  5659. <label>
  5660. <input type='radio' name='translation' value='openai'>
  5661. <span class='OJBetter_setting_menu_label_text'
  5662. data-i18n="settings:translation.options.services.openai.name">
  5663. <div class="help_tip">
  5664. ${helpCircleHTML}
  5665. <div class="tip_text"
  5666. data-i18n="[html]settings:translation.options.services.openai.helpText"></div>
  5667. </div>
  5668. </span>
  5669. </label>
  5670. </div>
  5671. <hr>
  5672. <h4>DeepL</h4>
  5673. <div class='OJBetter_setting_list'>
  5674. <label for="deepl_type" style="display: flex;" data-i18n="settings:translation.deepl.mode.title"></label>
  5675. <div class="help_tip">
  5676. ${helpCircleHTML}
  5677. <div class="tip_text" data-i18n="[html]settings:translation.deepl.mode.helpText"></div>
  5678. </div>
  5679. <select id="deepl_type" name="deepl_type">
  5680. <option value="free" data-i18n="settings:translation.deepl.mode.select.free"></option>
  5681. <option value="api" data-i18n="settings:translation.deepl.mode.select.api"></option>
  5682. </select>
  5683. </div>
  5684. <div id="deepl_config" class="config"></div>
  5685. <div class='OJBetter_setting_list'>
  5686. <label for="enableEmphasisProtection" data-i18n="settings:translation.deepl.enableEmphasisProtection.title"></label>
  5687. <div class="help_tip" style="margin-right: initial;">
  5688. ${helpCircleHTML}
  5689. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableEmphasisProtection.helpText"></div>
  5690. </div>
  5691. <div class="badge">Official API Only</div>
  5692. <input type="checkbox" id="enableEmphasisProtection" name="enableEmphasisProtection">
  5693. </div>
  5694. <div class='OJBetter_setting_list'>
  5695. <label for="enableLinkProtection" data-i18n="settings:translation.deepl.enableLinkProtection.title"></label>
  5696. <div class="help_tip" style="margin-right: initial;">
  5697. ${helpCircleHTML}
  5698. <div class="tip_text" data-i18n="[html]settings:translation.deepl.enableLinkProtection.helpText"></div>
  5699. </div>
  5700. <div class="badge">Official API Only</div>
  5701. <input type="checkbox" id="enableLinkProtection" name="enableLinkProtection">
  5702. </div>
  5703. <hr>
  5704. <h4>ChatGPT</h4>
  5705. <div id="chatgpt_config" class="config"></div>
  5706. <div class='OJBetter_setting_list'>
  5707. <label for="openai_isStream" data-i18n="settings:translation.chatgpt.isStream.name"></label>
  5708. <div class="help_tip">
  5709. ${helpCircleHTML}
  5710. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.isStream.helpText"></div>
  5711. </div>
  5712. <input type="checkbox" id="openai_isStream" name="openai_isStream">
  5713. </div>
  5714. <div class='OJBetter_setting_list'>
  5715. <label for="openai_asSystemPrompt" data-i18n="settings:translation.chatgpt.asSystemPrompt.name"></label>
  5716. <div class="help_tip">
  5717. ${helpCircleHTML}
  5718. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.asSystemPrompt.helpText"></div>
  5719. </div>
  5720. <input type="checkbox" id="openai_asSystemPrompt" name="openai_asSystemPrompt">
  5721. </div>
  5722. <div class="OJBetter_setting_list">
  5723. <label for='openai_customPrompt'>
  5724. <div style="display: flex;align-items: center;">
  5725. <span class="input_label" data-i18n="settings:translation.chatgpt.customPrompt.name"></span>
  5726. <div class="help_tip">
  5727. ${helpCircleHTML}
  5728. <div class="tip_text" data-i18n="[html]settings:translation.chatgpt.customPrompt.helpText"></div>
  5729. </div>
  5730. </div>
  5731. </label>
  5732. <textarea id="openai_customPrompt" placeholder='' require = false data-i18n="[placeholder]settings:translation.chatgpt.customPrompt.placeholder"></textarea>
  5733. </div>
  5734. <hr>
  5735. <h4 data-i18n="settings:translation.preference.title"></h4>
  5736. <div class='OJBetter_setting_list'>
  5737. <label for="comment_translation_choice" style="display: flex;"
  5738. data-i18n="settings:translation.preference.comment_translation_choice.title">
  5739. </label>
  5740. <select id="comment_translation_choice" name="comment_translation_choice">
  5741. <option value="0" data-i18n="settings:translation.preference.comment_translation_choice.services.follow"></option>
  5742. <option value="deepl" data-i18n="settings:translation.preference.comment_translation_choice.services.deepl"></option>
  5743. <option value="iflyrec" data-i18n="settings:translation.preference.comment_translation_choice.services.iflyrec"></option>
  5744. <option value="youdao" data-i18n="settings:translation.preference.comment_translation_choice.services.youdao"></option>
  5745. <option value="google" data-i18n="settings:translation.preference.comment_translation_choice.services.google"></option>
  5746. <option value="caiyun" data-i18n="settings:translation.preference.comment_translation_choice.services.caiyun"></option>
  5747. <option value="openai" data-i18n="settings:translation.preference.comment_translation_choice.services.openai"></option>
  5748. </select>
  5749. </div>
  5750. <hr>
  5751. <h4 data-i18n="settings:translation.autoTranslation.title"></h4>
  5752. <div class='OJBetter_setting_list'>
  5753. <label for="autoTranslation" data-i18n="settings:translation.autoTranslation.enable"></label>
  5754. <div class="help_tip">
  5755. ${helpCircleHTML}
  5756. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.helpText"></div>
  5757. </div>
  5758. <input type="checkbox" id="autoTranslation" name="autoTranslation">
  5759. </div>
  5760. <div class='OJBetter_setting_list'>
  5761. <label for='shortTextLength'>
  5762. <div style="display: flex;align-items: center;"
  5763. data-i18n="settings:translation.autoTranslation.shortTextLength.name"></div>
  5764. </label>
  5765. <div class="help_tip">
  5766. ${helpCircleHTML}
  5767. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.shortTextLength.helpText">
  5768. </div>
  5769. </div>
  5770. <input type='number' id='shortTextLength' class='no_default' require=true data-i18n="[placeholder]settings:translation.autoTranslation.shortTextLength.placeholder">
  5771. <span data-i18n="settings:translation.autoTranslation.shortTextLength.end"></span>
  5772. </div>
  5773. <div class='OJBetter_setting_list'>
  5774. <label for="allowMixTrans" data-i18n="settings:translation.autoTranslation.allowMixTrans.name"></label>
  5775. <div class="help_tip">
  5776. ${helpCircleHTML}
  5777. <div class="tip_text" data-i18n="[html]settings:translation.autoTranslation.allowMixTrans.helpText">
  5778. </div>
  5779. </div>
  5780. <input type="checkbox" id="allowMixTrans" name="allowMixTrans">
  5781. <div class='OJBetter_checkboxs'>
  5782. <input type="checkbox" id="deepl" name="mixedTranslation" value="deepl">
  5783. <label for="deepl" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.deepl"></label>
  5784. <input type="checkbox" id="iflyrec" name="mixedTranslation" value="iflyrec">
  5785. <label for="iflyrec" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.iflyrec"></label>
  5786. <input type="checkbox" id="youdao" name="mixedTranslation" value="youdao">
  5787. <label for="youdao" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.youdao"></label>
  5788. <input type="checkbox" id="google" name="mixedTranslation" value="google">
  5789. <label for="google" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.google">Google</label>
  5790. <input type="checkbox" id="caiyun" name="mixedTranslation" value="caiyun">
  5791. <label for="caiyun" data-i18n="settings:translation.autoTranslation.allowMixTrans.checkboxs.caiyun"></label>
  5792. </div>
  5793. </div>
  5794. <hr>
  5795. <h4 data-i18n="settings:translation.advanced.name"></h4>
  5796. <div class='OJBetter_setting_list'>
  5797. <label for="comment_translation_mode" style="display: flex;" data-i18n="settings:translation.advanced.mode.name"></label>
  5798. <div class="help_tip">
  5799. ${helpCircleHTML}
  5800. <div class="tip_text" data-i18n="[html]settings:translation.advanced.mode.helpText"></div>
  5801. </div>
  5802. <select id="comment_translation_mode" name="comment_translation_mode">
  5803. <option value="0" data-i18n="settings:translation.advanced.mode.options.0"></option>
  5804. <option value="1" data-i18n="settings:translation.advanced.mode.options.1"></option>
  5805. <option value="2" data-i18n="settings:translation.advanced.mode.options.2"></option>
  5806. </select>
  5807. </div>
  5808. <div class='OJBetter_setting_list'>
  5809. <label for="memoryTranslateHistory" data-i18n="settings:translation.advanced.memory.name"></label>
  5810. <div class="help_tip">
  5811. ${helpCircleHTML}
  5812. <div class="tip_text" data-i18n="[html]settings:translation.advanced.memory.helpText"></div>
  5813. </div>
  5814. <input type="checkbox" id="memoryTranslateHistory" name="memoryTranslateHistory">
  5815. </div>
  5816. <div class='OJBetter_setting_list'>
  5817. <label for="translation_retransAction" style="display: flex;" data-i18n="settings:translation.advanced.retrans.name"></label>
  5818. <div class="help_tip">
  5819. ${helpCircleHTML}
  5820. <div class="tip_text" data-i18n="[html]settings:translation.advanced.retrans.helpText"></div>
  5821. </div>
  5822. <select id="translation_retransAction" name="translation_retransAction">
  5823. <option value=0 data-i18n="settings:translation.advanced.retrans.options.0"></option>
  5824. <option value=1 data-i18n="settings:translation.advanced.retrans.options.1"></option>
  5825. </select>
  5826. </div>
  5827. <div class='OJBetter_setting_list'>
  5828. <label for='transWaitTime'>
  5829. <div style="display: flex;align-items: center;" data-i18n="settings:translation.advanced.transWaitTime.name"></div>
  5830. </label>
  5831. <div class="help_tip">
  5832. ${helpCircleHTML}
  5833. <div class="tip_text" data-i18n="[html]settings:translation.advanced.transWaitTime.helpText"></div>
  5834. </div>
  5835. <input type='number' id='transWaitTime' class='no_default' require=true data-i18n="[placeholder]settings:translation.advanced.transWaitTime.placeholder">
  5836. <span data-i18n="settings:translation.advanced.transWaitTime.end"></span>
  5837. </div>
  5838. <div class='OJBetter_setting_list'>
  5839. <label for="translation_replaceSymbol" style="display: flex;" data-i18n="settings:translation.advanced.replaceSymbol.name"></label>
  5840. <div class="help_tip">
  5841. ${helpCircleHTML}
  5842. <div class="tip_text" data-i18n="[html]settings:translation.advanced.replaceSymbol.helpText"></div>
  5843. </div>
  5844. <select id="translation_replaceSymbol" name="translation_replaceSymbol">
  5845. <option value=2 data-i18n="settings:translation.advanced.replaceSymbol.options.2"></option>
  5846. <option value=1 data-i18n="settings:translation.advanced.replaceSymbol.options.1"></option>
  5847. <option value=3 data-i18n="settings:translation.advanced.replaceSymbol.options.3"></option>
  5848. </select>
  5849. </div>
  5850. <div class='OJBetter_setting_list'>
  5851. <label for="filterTextWithoutEmphasis" data-i18n="settings:translation.advanced.filterTextWithoutEmphasis.name"></label>
  5852. <div class="help_tip">
  5853. ${helpCircleHTML}
  5854. <div class="tip_text" data-i18n="[html]settings:translation.advanced.filterTextWithoutEmphasis.helpText"></div>
  5855. </div>
  5856. <input type="checkbox" id="filterTextWithoutEmphasis" name="filterTextWithoutEmphasis">
  5857. </div>
  5858. <div class='OJBetter_setting_list'>
  5859. <label for="forceTurndownConversion" data-i18n="settings:translation.advanced.forceTurndownConversion.name"></label>
  5860. <div class="help_tip">
  5861. ${helpCircleHTML}
  5862. <div class="tip_text" data-i18n="[html]settings:translation.advanced.forceTurndownConversion.helpText"></div>
  5863. </div>
  5864. <input type="checkbox" id="forceTurndownConversion" name="forceTurndownConversion">
  5865. </div>
  5866. </div>
  5867. `;
  5868.  
  5869. const clist_rating_settings_HTML = `
  5870. <div id="clist_rating-settings" class="settings-page">
  5871. <h3 data-i18n="settings:clist.title"></h3>
  5872. <hr>
  5873. <h4 data-i18n="settings:clist.basics.name"></h4>
  5874. <div class='OJBetter_setting_list alert_tip'>
  5875. <div>
  5876. <p data-i18n="[html]settings:clist.basics.notice"></p>
  5877. </div>
  5878. </div>
  5879. <div class='OJBetter_setting_list'>
  5880. <label for='clist_Authorization'>
  5881. <div style="display: flex;align-items: center;">
  5882. <span class="input_label" data-i18n="settings:clist.basics.key.title"></span>
  5883. </div>
  5884. </label>
  5885. <div class="help_tip">
  5886. ${helpCircleHTML}
  5887. <div class="tip_text" data-i18n="[html]settings:clist.basics.key.helpText"></div>
  5888. </div>
  5889. <input type='text' id='clist_Authorization' class='no_default' required="true"
  5890. data-i18n="[placeholder]settings:clist.basics.key.keyPlaceholder">
  5891. </div>
  5892. <hr>
  5893. <h4 data-i18n="settings:clist.displayRating.title"></h4>
  5894. <div class='OJBetter_setting_list'>
  5895. <label for="showClistRating_contest"><span data-i18n="settings:clist.displayRating.contest.name"></span></label>
  5896. <input type="checkbox" id="showClistRating_contest" name="showClistRating_contest">
  5897. </div>
  5898. <div class='OJBetter_setting_list'>
  5899. <label for="showClistRating_problem"><span data-i18n="settings:clist.displayRating.problem.name"></span></label>
  5900. <input type="checkbox" id="showClistRating_problem" name="showClistRating_problem">
  5901. </div>
  5902. <div class='OJBetter_setting_list'>
  5903. <label for="showClistRating_problemset"><span data-i18n="settings:clist.displayRating.problemset.name"></span></label>
  5904. <input type="checkbox" id="showClistRating_problemset" name="showClistRating_problemset">
  5905. </div>
  5906. <hr>
  5907. <div class='OJBetter_setting_list'>
  5908. <label for="RatingHidden"><span data-i18n="settings:clist.spoilerProtection.title"></span></label>
  5909. <div class="help_tip">
  5910. ${helpCircleHTML}
  5911. <div class="tip_text" data-i18n="[html]settings:clist.spoilerProtection.helpText"></div>
  5912. </div>
  5913. <input type="checkbox" id="RatingHidden" name="RatingHidden">
  5914. </div>
  5915. </div>
  5916. `;
  5917.  
  5918. const code_editor_settings_HTML = `
  5919. <div id="code_editor-settings" class="settings-page">
  5920. <h3 data-i18n="settings:codeEditor.title"></h3>
  5921. <hr>
  5922. <h4 data-i18n="settings:codeEditor.basics"></h4>
  5923. <div class='OJBetter_setting_list'>
  5924. <label for="problemPageCodeEditor"><span
  5925. data-i18n="settings:codeEditor.problemPageCodeEditor.label"></span></label>
  5926. <div class="help_tip">
  5927. ${helpCircleHTML}
  5928. <div class="tip_text" data-i18n="settings:codeEditor.problemPageCodeEditor.helpText"></div>
  5929. </div>
  5930. <input type="checkbox" id="problemPageCodeEditor" name="problemPageCodeEditor">
  5931. </div>
  5932. <div class='OJBetter_setting_list'>
  5933. <label for="beautifyPreBlocks"><span
  5934. data-i18n="settings:codeEditor.beautifyPreBlocks.label"></span></label>
  5935. <div class="help_tip">
  5936. ${helpCircleHTML}
  5937. <div class="tip_text" data-i18n="settings:codeEditor.beautifyPreBlocks.helpText"></div>
  5938. </div>
  5939. <input type="checkbox" id="beautifyPreBlocks" name="beautifyPreBlocks">
  5940. </div>
  5941. <hr>
  5942. <h4 data-i18n="settings:codeEditor.preferences.title"></h4>
  5943. <div class='OJBetter_setting_list'>
  5944. <label for="isCodeSubmitConfirm"><span
  5945. data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.label"></span></label>
  5946. <div class="help_tip">
  5947. ${helpCircleHTML}
  5948. <div class="tip_text" data-i18n="settings:codeEditor.preferences.isCodeSubmitConfirm.helpText"></div>
  5949. </div>
  5950. <input type="checkbox" id="isCodeSubmitConfirm" name="isCodeSubmitConfirm">
  5951. </div>
  5952. <div class='OJBetter_setting_list'>
  5953. <label for="autoSubmitAfterPass"><span
  5954. data-i18n="settings:codeEditor.preferences.autoSubmitAfterPass.label"></span></label>
  5955. <div class="help_tip">
  5956. ${helpCircleHTML}
  5957. <div class="tip_text" data-i18n="settings:codeEditor.preferences.autoSubmitAfterPass.helpText"></div>
  5958. </div>
  5959. <input type="checkbox" id="autoSubmitAfterPass" name="autoSubmitAfterPass">
  5960. </div>
  5961. <div class='OJBetter_setting_list'>
  5962. <label for="alwaysConsumeMouseWheel"><span
  5963. data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.label"></span></label>
  5964. <div class="help_tip">
  5965. ${helpCircleHTML}
  5966. <div class="tip_text" data-i18n="settings:codeEditor.preferences.alwaysConsumeMouseWheel.helpText"></div>
  5967. </div>
  5968. <input type="checkbox" id="alwaysConsumeMouseWheel" name="alwaysConsumeMouseWheel">
  5969. </div>
  5970. <div class='OJBetter_setting_list'>
  5971. <label for="autoMemoryCode"><span
  5972. data-i18n="settings:codeEditor.preferences.autoMemoryCode.label"></span></label>
  5973. <div class="help_tip">
  5974. ${helpCircleHTML}
  5975. <div class="tip_text" data-i18n="settings:codeEditor.preferences.autoMemoryCode.helpText"></div>
  5976. </div>
  5977. <input type="checkbox" id="autoMemoryCode" name="autoMemoryCode">
  5978. </div>
  5979. <div class='OJBetter_setting_list'>
  5980. <label for="submitButtonPosition"><span
  5981. data-i18n="settings:codeEditor.preferences.submitButtonPosition.label"></span></label>
  5982. <div class="help_tip">
  5983. ${helpCircleHTML}
  5984. <div class="tip_text" data-i18n="settings:codeEditor.preferences.submitButtonPosition.helpText"></div>
  5985. </div>
  5986. <select id="submitButtonPosition" name="submitButtonPosition">
  5987. <option value="bottom" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.bottom"></option>
  5988. <option value="top" data-i18n="settings:codeEditor.preferences.submitButtonPosition.options.top"></option>
  5989. </select>
  5990. </div>
  5991. <hr>
  5992. <h4 data-i18n="settings:codeEditor.onlineCodeExecution.title"></h4>
  5993. <label>
  5994. <input type='radio' name='compiler' value='official'>
  5995. <span class='OJBetter_setting_menu_label_text'
  5996. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.codeforces"></span>
  5997. </label>
  5998. <label>
  5999. <input type='radio' name='compiler' value='wandbox'>
  6000. <span class='OJBetter_setting_menu_label_text'
  6001. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.wandbox"></span>
  6002. </label>
  6003. <label>
  6004. <input type='radio' name='compiler' value='rextester'>
  6005. <span class='OJBetter_setting_menu_label_text'
  6006. data-i18n="settings:codeEditor.onlineCodeExecution.compilerOptions.rextester"></span>
  6007. </label>
  6008. <hr>
  6009. <h4 data-i18n="settings:codeEditor.lsp.title"></h4>
  6010. <div class='OJBetter_setting_list'>
  6011. <label for="useLSP"><span data-i18n="settings:codeEditor.lsp.useLSP.label"></span></label>
  6012. <div class="help_tip">
  6013. ${helpCircleHTML}
  6014. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.useLSP.helpText"></div>
  6015. </div>
  6016. <input type="checkbox" id="useLSP" name="useLSP">
  6017. </div>
  6018. <div class='OJBetter_setting_list'>
  6019. <label for='OJBetter_Bridge_WorkUri'>
  6020. <div style="display: flex;align-items: center;">
  6021. <span class="input_label" data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.label"></span>
  6022. </div>
  6023. </label>
  6024. <div class="help_tip">
  6025. ${helpCircleHTML}
  6026. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.helpText"></div>
  6027. </div>
  6028. <input type='text' id='OJBetter_Bridge_WorkUri' class='no_default'
  6029. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_WorkUri.placeholder">
  6030. </div>
  6031. <div class='OJBetter_setting_list'>
  6032. <label for='OJBetter_Bridge_SocketUrl'>
  6033. <div style="display: flex;align-items: center;">
  6034. <span class="input_label"
  6035. data-i18n="settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.label"></span>
  6036. </div>
  6037. </label>
  6038. <div class="help_tip">
  6039. ${helpCircleHTML}
  6040. <div class="tip_text" data-i18n="[html]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.helpText"></div>
  6041. </div>
  6042. <input type='text' id='OJBetter_Bridge_SocketUrl' class='no_default'
  6043. require=true data-i18n="[placeholder]settings:codeEditor.lsp.OJBetter_Bridge_SocketUrl.placeholder">
  6044. </div>
  6045. <hr>
  6046. <h4 data-i18n="settings:codeEditor.staticCompletionEnhancement.title"></h4>
  6047. <div class='OJBetter_setting_list'>
  6048. <label for="cppCodeTemplateComplete"><span
  6049. data-i18n="settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.label"></span></label>
  6050. <div class="help_tip">
  6051. ${helpCircleHTML}
  6052. <div class="tip_text" data-i18n="[html]settings:codeEditor.staticCompletionEnhancement.cppCodeTemplateComplete.helpText"></div>
  6053. </div>
  6054. <input type="checkbox" id="cppCodeTemplateComplete" name="cppCodeTemplateComplete">
  6055. </div>
  6056. <hr>
  6057. <h5 data-i18n="settings:codeEditor.staticCompletionEnhancement.customization"></h5>
  6058. <div class='OJBetter_setting_list alert_warn'>
  6059. <div>
  6060. <p data-i18n="settings:codeEditor.staticCompletionEnhancement.performanceWarning"></p>
  6061. </div>
  6062. </div>
  6063. <div id="Complet_config" class="config"></div>
  6064. </div>
  6065. `;
  6066.  
  6067. const preference_settings_HTML = `
  6068. <div id="preference-settings" class="settings-page">
  6069. <h3 data-i18n="settings:preference.title"></h3>
  6070. <hr>
  6071. <div class='OJBetter_setting_list'>
  6072. <label for="showLoading" data-i18n="settings:preference.loadingInfo.label"></label>
  6073. <div class="help_tip">
  6074. ${helpCircleHTML}
  6075. <div class="tip_text" data-i18n="[html]settings:preference.loadingInfo.helpText" data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }'></div>
  6076. </div>
  6077. <input type="checkbox" id="showLoading" name="showLoading">
  6078. </div>
  6079. <div class='OJBetter_setting_list'>
  6080. <label for="hoverTargetAreaDisplay" data-i18n="settings:preference.targetArea.label"></label>
  6081. <div class="help_tip">
  6082. ${helpCircleHTML}
  6083. <div class="tip_text" data-i18n="[html]settings:preference.targetArea.helpText"></div>
  6084. </div>
  6085. <input type="checkbox" id="hoverTargetAreaDisplay" name="hoverTargetAreaDisplay">
  6086. </div>
  6087. <div class='OJBetter_setting_list'>
  6088. <label for='iconButtonSize'>
  6089. <div style="display: flex;align-items: center;" data-i18n="settings:preference.iconButtonSize.title"></div>
  6090. </label>
  6091. <div class="help_tip">
  6092. ${helpCircleHTML}
  6093. <div class="tip_text" data-i18n="[html]settings:preference.iconButtonSize.helpText"></div>
  6094. </div>
  6095. <input type='number' id='iconButtonSize' class='no_default' require=true data-i18n="[placeholder]settings:preference.iconButtonSize.placeholder">
  6096. <span>px</span>
  6097. </div>
  6098. </div>
  6099. `;
  6100.  
  6101. const dev_settings_HTML = `
  6102. <div id="dev-settings" class="settings-page">
  6103. <h3 data-i18n="settings:dev.title"></h3>
  6104. <hr>
  6105. <div class='OJBetter_setting_list alert_danger'>
  6106. <div>
  6107. <p data-i18n="[html]settings:dev.notice"></p>
  6108. </div>
  6109. </div>
  6110. <hr>
  6111. <h5 data-i18n="settings:dev.load.title"></h5>
  6112. <div class='OJBetter_setting_list'>
  6113. <label for="notWaiteLoaded"><span data-i18n="settings:dev.load.notWaiteLoaded.label"></span></label>
  6114. <div class="help_tip">
  6115. ${helpCircleHTML}
  6116. <div class="tip_text" data-i18n="[html]settings:dev.load.notWaiteLoaded.helpText"></div>
  6117. </div>
  6118. <input type="checkbox" id="notWaiteLoaded" name="notWaiteLoaded">
  6119. </div>
  6120. <hr>
  6121. <h5 data-i18n="settings:dev.l10n.title"></h5>
  6122. <div class='OJBetter_setting_list'>
  6123. <label><span data-i18n="settings:dev.l10n.refreshScrpitCache.label"></span></label>
  6124. <div class="help_tip">
  6125. ${helpCircleHTML}
  6126. <div class="tip_text" data-i18n="[html]settings:dev.l10n.refreshScrpitCache.helpText"></div>
  6127. </div>
  6128. <button type="button" id="l10n_refreshScrpitCacheButton" name="l10n_refreshScrpitCacheButton" data-i18n="settings:dev.l10n.refreshScrpitCache.button"></button>
  6129. </div>
  6130. <hr>
  6131. <h5 data-i18n="settings:dev.l10n_web.title"></h5>
  6132. <div class='OJBetter_setting_list'>
  6133. <label><span data-i18n="settings:dev.l10n_web.refreshScrpitCache.label"></span></label>
  6134. <div class="help_tip">
  6135. ${helpCircleHTML}
  6136. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.refreshScrpitCache.helpText"></div>
  6137. </div>
  6138. <button type="button" id="l10n_web_refreshScrpitCacheButton" name="l10n_web_refreshScrpitCacheButton" data-i18n="settings:dev.l10n_web.refreshScrpitCache.button"></button>
  6139. </div>
  6140. <div class='OJBetter_setting_list'>
  6141. <label for="isRuleMarkingEnabled"><span data-i18n="settings:dev.l10n_web.isRuleMarkingEnabled.label"></span></label>
  6142. <div class="help_tip">
  6143. ${helpCircleHTML}
  6144. <div class="tip_text" data-i18n="[html]settings:dev.l10n_web.isRuleMarkingEnabled.helpText"></div>
  6145. </div>
  6146. <input type="checkbox" id="isRuleMarkingEnabled" name="isRuleMarkingEnabled">
  6147. </div>
  6148. <hr>
  6149. <h5 data-i18n="settings:dev.indexedDB.title"></h5>
  6150. <div class='OJBetter_setting_list'>
  6151. <label><span data-i18n="settings:dev.indexedDB.clear.label"></span></label>
  6152. <div class="help_tip">
  6153. ${helpCircleHTML}
  6154. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.clear.helpText"></div>
  6155. </div>
  6156. <button type="button" id="indexedDB_clearButton" name="indexedDB_clearButton" data-i18n="settings:dev.indexedDB.clear.button"></button>
  6157. </div>
  6158. <div class='OJBetter_setting_list'>
  6159. <label><span data-i18n="settings:dev.indexedDB.inputOrExport.label"></span></label>
  6160. <div class="help_tip">
  6161. ${helpCircleHTML}
  6162. <div class="tip_text" data-i18n="[html]settings:dev.indexedDB.inputOrExport.helpText"></div>
  6163. </div>
  6164. <button type="button" id="indexedDB_exportButton" name="indexedDB_exportButton" data-i18n="settings:dev.indexedDB.inputOrExport.export"></button>
  6165. <button type="button" id="indexedDB_importButton" name="indexedDB_importButton" data-i18n="settings:dev.indexedDB.inputOrExport.import"></button>
  6166. </div>
  6167. <hr>
  6168. <h5 data-i18n="settings:dev.configuration.title"></h5>
  6169. <div class='OJBetter_setting_list'>
  6170. <label><span data-i18n="settings:dev.configuration.clear.label"></span></label>
  6171. <div class="help_tip">
  6172. ${helpCircleHTML}
  6173. <div class="tip_text" data-i18n="[html]settings:dev.configuration.clear.helpText"></div>
  6174. </div>
  6175. <button type="button" id="configuration_clearButton" name="configuration_clearButton" data-i18n="settings:dev.configuration.clear.button"></button>
  6176. </div>
  6177. <div class='OJBetter_setting_list'>
  6178. <label><span data-i18n="settings:dev.configuration.inputOrExport.label"></span></label>
  6179. <div class="help_tip">
  6180. ${helpCircleHTML}
  6181. <div class="tip_text" data-i18n="[html]settings:dev.configuration.inputOrExport.helpText"></div>
  6182. </div>
  6183. <button type="button" id="configuration_exportButton" name="configuration_exportButton" data-i18n="settings:dev.configuration.inputOrExport.export"></button>
  6184. <button type="button" id="configuration_importButton" name="configuration_importButton" data-i18n="settings:dev.configuration.inputOrExport.import"></button>
  6185. </div>
  6186. </div>
  6187. `;
  6188.  
  6189. const about_settings_HTML = `
  6190. <div id="about-settings" class="settings-page">
  6191. <h3 data-i18n="settings:about.title"></h3>
  6192. <hr>
  6193. <div class='versionInfo'>
  6194. <p>${OJBetter.state.name}</p>
  6195. <p><span data-i18n="settings:about.version"></span><span id="nowVersion">${OJBetter.state.version}</span></p>
  6196. <p> @北极小狐 <a target="_blank" href="https://github.com/beijixiaohu/OJBetter">Github</a>
  6197. <a target="_blank" href="https://greasyfork.org/zh-CN/scripts/465777">GreasyFork</a></p>
  6198. </div>
  6199. <hr>
  6200. <h5 data-i18n="settings:about.update.title"></h5>
  6201. <div id="thanksforDevChannelNotice" class='OJBetter_setting_list alert_info'>
  6202. <div data-i18n="[html]settings:about.update.thanksforDevChannelNotice"} data-i18n-options='{ "scriptName": "${OJBetter.state.name}" }' ></div>
  6203. </div>
  6204. <div class='OJBetter_setting_list'>
  6205. <label for="updateChannel"><span data-i18n="settings:about.update.channel.label"></span></label>
  6206. <div class="help_tip">
  6207. ${helpCircleHTML}
  6208. <div class="tip_text" data-i18n="[html]settings:about.update.channel.helpText"></div>
  6209. </div>
  6210. <select id="updateChannel" name="updateChannel">
  6211. <option value="release" data-i18n="settings:about.update.channel.options.release"></option>
  6212. <option value="dev" data-i18n="settings:about.update.channel.options.dev"></option>
  6213. </select>
  6214. </div>
  6215. <div class='OJBetter_setting_list'>
  6216. <label for="updateSource"><span data-i18n="settings:about.update.source.label"></span></label>
  6217. <div class="help_tip">
  6218. ${helpCircleHTML}
  6219. <div class="tip_text" data-i18n="[html]settings:about.update.source.helpText"></div>
  6220. </div>
  6221. <select id="updateSource" name="updateSource">
  6222. <option value="greasyfork" data-i18n="settings:about.update.source.options.greasyfork"></option>
  6223. <option value="github" data-i18n="settings:about.update.source.options.github"></option>
  6224. <option value="aliyunoss" data-i18n="settings:about.update.source.options.aliyunoss"></option>
  6225. </select>
  6226. </div>
  6227. </div>
  6228. `;
  6229.  
  6230. const OJBetter_setting_content_HTML = `
  6231. <div class="OJBetter_setting_content">
  6232. ${basic_settings_HTML}
  6233. ${l10n_settings_HTML}
  6234. ${translation_settings_HTML}
  6235. ${clist_rating_settings_HTML}
  6236. ${code_editor_settings_HTML}
  6237. ${preference_settings_HTML}
  6238. ${dev_settings_HTML}
  6239. ${about_settings_HTML}
  6240. </div>
  6241. `;
  6242.  
  6243. // 设置界面HTML
  6244. const OJBetterSettingMenu_HTML = `
  6245. <dialog class='OJBetter_setting_menu' id='OJBetter_setting_menu'>
  6246. <div class="tool-box">
  6247. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6248. <i class="iconfont">&#xe614;</i>
  6249. </button>
  6250. </div>
  6251. <div class="OJBetter_setting_container">
  6252. ${OJBetter_setting_sidebar_HTML}
  6253. ${OJBetter_setting_content_HTML}
  6254. </div>
  6255. </dialog>
  6256. `;
  6257.  
  6258. const apiCustomConfigHTML = (prefix) => {
  6259. return `
  6260. <div class="OJBetter_setting_list">
  6261. <label for='${prefix}_header'>
  6262. <div style="display: flex;align-items: center;">
  6263. <span class="input_label" data-i18n="config:common.advanced.header.label"></span>
  6264. <div class="help_tip">
  6265. ${helpCircleHTML}
  6266. <div class="tip_text" data-i18n="[html]config:common.advanced.header.tipText"></div>
  6267. </div>
  6268. </div>
  6269. </label>
  6270. <textarea id="${prefix}_header" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.header.placeholder"></textarea>
  6271. </div>
  6272. <div class="OJBetter_setting_list">
  6273. <label for='${prefix}_data'>
  6274. <div style="display: flex;align-items: center;">
  6275. <span class="input_label" data-i18n="config:common.advanced.data.label"></span>
  6276. <div class="help_tip">
  6277. ${helpCircleHTML}
  6278. <div class="tip_text" data-i18n="[html]config:common.advanced.data.tipText"></div>
  6279. </div>
  6280. </div>
  6281. </label>
  6282. <textarea id="${prefix}_data" placeholder='' require = false data-i18n="[placeholder]config:common.advanced.data.placeholder"></textarea>
  6283. </div>
  6284. `;
  6285. };
  6286.  
  6287. const apiQuotaConfigHTML = (prefix) => {
  6288. return `
  6289. <div class="OJBetter_setting_list">
  6290. <label for='${prefix}_quota_url'>
  6291. <div style="display: flex;align-items: center;">
  6292. <span class="input_label" data-i18n="config:common.quota.url.label"></span>
  6293. <div class="help_tip">
  6294. ${helpCircleHTML}
  6295. <div class="tip_text" data-i18n="[html]config:common.quota.url.tipText"></div>
  6296. </div>
  6297. </div>
  6298. </label>
  6299. <input type='text' id='${prefix}_quota_url' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.url.placeholder">
  6300. </div>
  6301. <div class="OJBetter_setting_list">
  6302. <label for="${prefix}_quota_method" style="display: flex;" data-i18n="config:common.quota.method.label"></label>
  6303. <div class="help_tip">
  6304. ${helpCircleHTML}
  6305. <div class="tip_text" data-i18n="[html]config:common.quota.method.tipText"></div>
  6306. </div>
  6307. <select id="${prefix}_quota_method" name="${prefix}_quota_method">
  6308. <option value="get">GET</option>
  6309. <option value="post">POST</option>
  6310. </select>
  6311. </div>
  6312. <div class="OJBetter_setting_list">
  6313. <label for='${prefix}_quota_header'>
  6314. <div style="display: flex;align-items: center;">
  6315. <span class="input_label" data-i18n="config:common.quota.header.label"></span>
  6316. <div class="help_tip">
  6317. ${helpCircleHTML}
  6318. <div class="tip_text" data-i18n="[html]config:common.quota.header.tipText"></div>
  6319. </div>
  6320. </div>
  6321. </label>
  6322. <textarea id="${prefix}_quota_header" placeholder='' require = false data-i18n="[placeholder]config:common.quota.header.placeholder"></textarea>
  6323. </div>
  6324. <div class="OJBetter_setting_list">
  6325. <label for='${prefix}_quota_data'>
  6326. <div style="display: flex;align-items: center;">
  6327. <span class="input_label" data-i18n="config:common.quota.data.label"></span>
  6328. <div class="help_tip">
  6329. ${helpCircleHTML}
  6330. <div class="tip_text" data-i18n="[html]config:common.quota.data.tipText"></div>
  6331. </div>
  6332. </div>
  6333. </label>
  6334. <textarea id="${prefix}_quota_data" placeholder='' require = false data-i18n="[placeholder]config:common.quota.data.placeholder"></textarea>
  6335. </div>
  6336. <div class="OJBetter_setting_list">
  6337. <div style="display: flex;align-items: center;">
  6338. <span class="input_label" data-i18n="config:common.quota.surplus.label"></span>
  6339. <div class="help_tip">
  6340. ${helpCircleHTML}
  6341. <div class="tip_text" data-i18n="[html]config:common.quota.surplus.tipText"></div>
  6342. </div>
  6343. </div>
  6344. </label>
  6345. <input type='text' id='${prefix}_quota_surplus' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:common.quota.surplus.placeholder">
  6346. </div>
  6347. `;
  6348. }
  6349.  
  6350. const deeplConfigEditHTML = `
  6351. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6352. <div class='OJBetter_setting_content'>
  6353. <div class="tool-box">
  6354. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6355. <i class="iconfont">&#xe614;</i>
  6356. </button>
  6357. </div>
  6358. <h4 data-i18n="config:deepl.title"></h4>
  6359. <h5 data-i18n="config:deepl.basic.title"></h5>
  6360. <hr>
  6361. <div class="OJBetter_setting_list">
  6362. <label for='name'>
  6363. <span class="input_label" data-i18n="config:deepl.basic.name.label"></span>
  6364. </label>
  6365. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.name.placeholder">
  6366. </div>
  6367. <div class='OJBetter_setting_list'>
  6368. <label for="deepl_apiGenre" style="display: flex;" data-i18n="config:deepl.genre.label"></label>
  6369. <div class="help_tip">
  6370. ${helpCircleHTML}
  6371. <div class="tip_text" data-i18n="[html]config:deepl.genre.tipText"></div>
  6372. </div>
  6373. <select id="deepl_apiGenre" name="deepl_apiGenre">
  6374. <option value="api-free">api-free</option>
  6375. <option value="api-pro">api-pro</option>
  6376. <option value="deeplx">deeplx</option>
  6377. </select>
  6378. </div>
  6379. <div class="OJBetter_setting_list">
  6380. <label for='deepl_key'>
  6381. <div style="display: flex;align-items: center;">
  6382. <span class="input_label" data-i18n="config:deepl.basic.key.label"></span>
  6383. <div class="help_tip">
  6384. ${helpCircleHTML}
  6385. <div class="tip_text" data-i18n="[html]config:deepl.basic.key.tipText"></div>
  6386. </div>
  6387. </div>
  6388. </label>
  6389. <input type='text' id='deepl_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:deepl.basic.key.placeholder">
  6390. </div>
  6391. <div class="OJBetter_setting_list">
  6392. <label for='deepl_proxy'>
  6393. <div style="display: flex;align-items: center;">
  6394. <span class="input_label" data-i18n="config:deepl.basic.proxy.label">Proxy API:</span>
  6395. <div class="help_tip">
  6396. ${helpCircleHTML}
  6397. <div class="tip_text" data-i18n="[html]config:deepl.basic.proxy.tipText"></div>
  6398. </div>
  6399. </div>
  6400. </label>
  6401. <input type='text' id='deepl_proxy' placeholder='' require = false>
  6402. </div>
  6403. <hr>
  6404. <details>
  6405. <summary data-i18n="config:common.advanced.title"></summary>
  6406. ${apiCustomConfigHTML('deepl')}
  6407. </details>
  6408. <details>
  6409. <summary data-i18n="config:common.quota.title"></summary>
  6410. ${apiQuotaConfigHTML('deepl')}
  6411. </details>
  6412. <button id='tempConfig_save' data-i18n="common:save"></button>
  6413. </div>
  6414. </dialog>
  6415. `;
  6416.  
  6417. const chatgptConfigEditHTML = `
  6418. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6419. <div class='OJBetter_setting_content'>
  6420. <div class="tool-box">
  6421. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6422. <i class="iconfont">&#xe614;</i>
  6423. </button>
  6424. </div>
  6425. <h4 data-i18n="config:chatgpt.title"></h4>
  6426. <h5 data-i18n="config:chatgpt.basic.title"></h5>
  6427. <hr>
  6428. <div class="OJBetter_setting_list">
  6429. <label for='name'>
  6430. <span class="input_label" data-i18n="config:chatgpt.basic.name.label"></span>
  6431. </label>
  6432. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.name.placeholder">
  6433. </div>
  6434. <div class="OJBetter_setting_list">
  6435. <label for='chatgpt_model'>
  6436. <div style="display: flex;align-items: center;">
  6437. <span class="input_label" data-i18n="[html]config:chatgpt.basic.model.label"></span>
  6438. <div class="help_tip">
  6439. ${helpCircleHTML}
  6440. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.model.tipText"></div>
  6441. </div>
  6442. </div>
  6443. </label>
  6444. <input type='text' id='chatgpt_model' placeholder='gpt-3.5-turbo' require = false>
  6445. </div>
  6446. <div class="OJBetter_setting_list">
  6447. <label for='chatgpt_key'>
  6448. <div style="display: flex;align-items: center;">
  6449. <span class="input_label" data-i18n="config:chatgpt.basic.key.label"></span>
  6450. <div class="help_tip">
  6451. ${helpCircleHTML}
  6452. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.key.tipText"></div>
  6453. </div>
  6454. </div>
  6455. </label>
  6456. <input type='text' id='chatgpt_key' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:chatgpt.basic.key.placeholder">
  6457. </div>
  6458. <div class="OJBetter_setting_list">
  6459. <label for='chatgpt_proxy'>
  6460. <div style="display: flex;align-items: center;">
  6461. <span class="input_label" data-i18n="config:chatgpt.basic.proxy.label">Proxy API:</span>
  6462. <div class="help_tip">
  6463. ${helpCircleHTML}
  6464. <div class="tip_text" data-i18n="[html]config:chatgpt.basic.proxy.tipText"></div>
  6465. </div>
  6466. </div>
  6467. </label>
  6468. <input type='text' id='chatgpt_proxy' placeholder='https://api.openai.com/v1/chat/completions' require = false>
  6469. </div>
  6470. <hr>
  6471. <details>
  6472. <summary data-i18n="config:common.advanced.title"></summary>
  6473. ${apiCustomConfigHTML('chatgpt')}
  6474. </details>
  6475. <details>
  6476. <summary data-i18n="config:common.quota.title"></summary>
  6477. ${apiQuotaConfigHTML('chatgpt')}
  6478. </details>
  6479. <button id='tempConfig_save' data-i18n="common:save"></button>
  6480. </div>
  6481. </dialog>
  6482. `;
  6483.  
  6484. const CompletConfigEditHTML = `
  6485. <dialog class='OJBetter_setting_menu' id='config_edit_menu'>
  6486. <div class='OJBetter_setting_content'>
  6487. <div class="tool-box">
  6488. <button class='ojb_btn ojb_btn_popover top btn-close'>
  6489. <i class="iconfont">&#xe614;</i>
  6490. </button>
  6491. </div>
  6492. <h4 data-i18n="config:complet.title"></h4>
  6493. <hr>
  6494. <div class="OJBetter_setting_list">
  6495. <label for='name'>
  6496. <span class="input_label" data-i18n="config:complet.name.label"></span>
  6497. </label>
  6498. <input type='text' id='name' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.name.placeholder">
  6499. </div>
  6500. <div class='OJBetter_setting_list'>
  6501. <label for="complet_isChoose"><span id="loaded_span" data-i18n="config:complet.choose.label"></span></label>
  6502. <input type="checkbox" id="complet_isChoose" name="complet_isChoose" require = false>
  6503. </div>
  6504. <div class='OJBetter_setting_list'>
  6505. <label for="complet_genre" style="display: flex;" data-i18n="config:complet.genre.label"></label>
  6506. <div class="help_tip">
  6507. ${helpCircleHTML}
  6508. <div class="tip_text" data-i18n="[html]config:complet.genre.tipText"></div>
  6509. </div>
  6510. <select id="complet_genre" name="complet_genre">
  6511. <option value="monaco">monaco</option>
  6512. <option value="ace">ace</option>
  6513. </select>
  6514. </div>
  6515. <div class='OJBetter_setting_list'>
  6516. <label for="complet_language" style="display: flex;" data-i18n="config:complet.language.label"></label>
  6517. <select id="complet_language" name="complet_language">
  6518. <option value="cpp">cpp</option>
  6519. <option value="python">python</option>
  6520. <option value="java">java</option>
  6521. <option value="c">c</option>
  6522. </select>
  6523. </div>
  6524. <div class="OJBetter_setting_list">
  6525. <label for='complet_jsonUrl'>
  6526. <div style="display: flex;align-items: center;">
  6527. <span class="input_label">JSON URL:</span>
  6528. <div class="help_tip">
  6529. ${helpCircleHTML}
  6530. <div class="tip_text" data-i18n="[html]config:complet.jsonurl.tipText"></div>
  6531. </div>
  6532. </div>
  6533. </label>
  6534. <div class='OJBetter_setting_list alert_warn' data-i18n="[html]config:complet.jsonurl.alert"></div>
  6535. <input type='text' id='complet_jsonUrl' class='no_default' placeholder='' require = true data-i18n="[placeholder]config:complet.jsonurl.placeholder">
  6536. </div>
  6537. <button id='tempConfig_save' data-i18n="common:save"></button>
  6538. </div>
  6539. </dialog>
  6540. `;
  6541.  
  6542. /**
  6543. * 加载设置按钮面板
  6544. */
  6545. async function initSettingsPanel() {
  6546. /**
  6547. * 添加右上角设置按钮
  6548. * @param {string} location 位置选择器
  6549. * @param {string} method 插入方法
  6550. */
  6551. function insertOJBetterSettingButton(location, method) {
  6552. $(location)[method](`<button class='ojb_btn OJBetter_setting'>
  6553. ${OJBetter.state.name} ${i18next.t('settings', { ns: 'common' })}</button>`);
  6554. }
  6555.  
  6556. /**
  6557. * ============================================
  6558. * 该网站插入设置按钮的位置和方式
  6559. */
  6560. insertOJBetterSettingButton(".lang-chooser", "before");
  6561. insertOJBetterSettingButton(".enter-or-register-box", "after");
  6562. if (OJBetter.typeOfPage.is_completeProblemset) insertOJBetterSettingButton(".lang", "before");
  6563. /**
  6564. * ============================================
  6565. */
  6566.  
  6567. const $settingBtns = $(".OJBetter_setting");
  6568. $settingBtns.click(() => {
  6569. $settingBtns.prop("disabled", true).addClass("open");
  6570.  
  6571. // 设置面板div
  6572. const settingMenu = OJB_safeCreateJQElement(OJBetterSettingMenu_HTML);
  6573. $("body").append(settingMenu);
  6574.  
  6575. elementLocalize(settingMenu); // 加载i18n
  6576. OJB_showModal(settingMenu);
  6577. OJB_addDraggable($('#OJBetter_setting_menu')); // 窗口支持拖拽
  6578.  
  6579. // help帮助悬浮窗位置更新
  6580. $(document).on('mouseenter', '.help-icon', function (event) {
  6581. var menuOffset = $('.OJBetter_setting_menu:last').offset();
  6582. var mouseX = event.pageX - menuOffset.left;
  6583. var mouseY = event.pageY - menuOffset.top;
  6584.  
  6585. $('.tip_text').css({
  6586. 'top': mouseY + 'px',
  6587. 'left': mouseX + 'px'
  6588. });
  6589. });
  6590.  
  6591. // 选项卡切换
  6592. $('.OJBetter_setting_sidebar a').click(function (event) {
  6593. event.preventDefault();
  6594. $('.OJBetter_setting_sidebar a').removeClass('active');
  6595. $('.settings-page').removeClass('active');
  6596. $(this).addClass('active');
  6597. const targetPageId = $(this).attr('href').substring(1);
  6598. $('#' + targetPageId).addClass('active');
  6599. });
  6600.  
  6601. /**
  6602. * 更新单选按钮组的可用状态
  6603. * @param {string} selector 单选按钮组的选择器
  6604. * @param {string} targetLanguage 目标语言
  6605. * @param {Object} translationSupport 翻译支持的语言对应表
  6606. */
  6607. const updateRadioButtonsAvailability = (selector, targetLanguage) => {
  6608. Object.entries(OJBetter.supportList.translationSupport).forEach(([service, languages]) => {
  6609. const radioButton = $(selector).find(`input[value="${service}"]`);
  6610. const isEnabled = languages[targetLanguage];
  6611. $(radioButton).prop('disabled', !isEnabled);
  6612. if (!isEnabled) {
  6613. $(radioButton).prop('checked', false);
  6614. }
  6615. });
  6616. };
  6617.  
  6618. /**
  6619. * 检查下拉框选中项是否有效,若无效则清空
  6620. * @param {string} selector 下拉框的选择器
  6621. */
  6622. const validateSelectOption = (selector) => {
  6623. const selectedValue = $(selector).val();
  6624. if (!selectedValue) {
  6625. $(selector).val('');
  6626. }
  6627. };
  6628.  
  6629. /**
  6630. * 更新翻译目标语言下拉框的可用状态
  6631. * @param {string} selector 下拉框的选择器
  6632. * @param {string} targetLanguage 目标语言
  6633. * @param {Object} translationSupport 翻译支持的语言对应表
  6634. */
  6635. const updateSelectOptionsAvailability = (selector, targetLanguage) => {
  6636. $(selector).children('option').each(function () {
  6637. const optionValue = $(this).val();
  6638. const isEnabled = OJBetter.supportList.translationSupport[optionValue] ? OJBetter.supportList.translationSupport[optionValue][targetLanguage] : true;
  6639. $(this).prop('disabled', !isEnabled);
  6640. });
  6641. validateSelectOption(selector);
  6642. };
  6643.  
  6644. /**
  6645. * 更新翻译服务复选框的可用状态
  6646. * @param {string} selector 复选框的选择器
  6647. * @param {string} targetLanguage 目标语言
  6648. * @param {Object} translationSupport 翻译支持的语言对应表
  6649. */
  6650. const updateCheckboxesAvailability = (selector, targetLanguage) => {
  6651. $(selector).children('input').each(function () {
  6652. const checkboxValue = $(this).val();
  6653. const isEnabled = OJBetter.supportList.translationSupport[checkboxValue][targetLanguage];
  6654. $(this).prop('disabled', !isEnabled);
  6655. if (!isEnabled) {
  6656. $(this).prop('checked', false);
  6657. }
  6658. });
  6659. };
  6660.  
  6661. /**
  6662. * 更新更新源下拉框的可用状态
  6663. * @param {string} selector 下拉框的选择器
  6664. * @param {string} targetLanguage 目标语言
  6665. * @param {Object} translationSupport 翻译支持的语言对应表
  6666. */
  6667. const updateUpdateSourceSelectOptionsAvailability = (selector, updateChannel) => {
  6668. $(selector).children('option').each(function () {
  6669. const optionValue = $(this).val();
  6670. const isEnabled = OJBetter.supportList.updateSourceSupportList[optionValue][updateChannel];
  6671. $(this).prop('disabled', !isEnabled);
  6672. });
  6673. validateSelectOption(selector);
  6674. };
  6675.  
  6676. /**
  6677. * 创建配置结构
  6678. * @param {string} type - 该字段的在表单中的类型
  6679. * @param {string} value - 在配置中的键值
  6680. * @param {boolean} require - 是否是表单的必填项
  6681. * @param {string} [check=""] check - 调用的合法性检查
  6682. */
  6683. function createStructure(type, value, require, check = "") {
  6684. return { type, value, require, check };
  6685. }
  6686.  
  6687. // deepl配置
  6688. const deeplStructure = {
  6689. '#name': createStructure('text', 'name', true),
  6690. '#deepl_apiGenre': createStructure('text', 'apiGenre', true),
  6691. '#deepl_key': createStructure('text', 'key', false),
  6692. '#deepl_proxy': createStructure('text', 'proxy', false),
  6693. '#deepl_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6694. '#deepl_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6695. '#deepl_quota_url': createStructure('text', 'quota_url', false),
  6696. '#deepl_quota_method': createStructure('text', 'quota_method', false),
  6697. '#deepl_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6698. '#deepl_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6699. '#deepl_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6700. };
  6701. let tempConfig_deepl = GM_getValue('deepl_config'); // 获取配置信息
  6702. const configManager_deepl = new ConfigManager('#deepl_config', 'deepl_config_', tempConfig_deepl, deeplStructure, deeplConfigEditHTML);
  6703. configManager_deepl.registerChoiceChange();
  6704.  
  6705. // chatgpt配置
  6706. const chatgptStructure = {
  6707. '#name': createStructure('text', 'name', true),
  6708. '#chatgpt_model': createStructure('text', 'model', false),
  6709. '#chatgpt_key': createStructure('text', 'key', true),
  6710. '#chatgpt_proxy': createStructure('text', 'proxy', false),
  6711. '#chatgpt_header': createStructure('text', '_header', false, 'keyValuePairs'),
  6712. '#chatgpt_data': createStructure('text', '_data', false, 'keyValuePairs'),
  6713. '#chatgpt_quota_url': createStructure('text', 'quota_url', false),
  6714. '#chatgpt_quota_header': createStructure('text', 'quota_header', false, 'keyValuePairs'),
  6715. '#chatgpt_quota_data': createStructure('text', 'quota_data', false, 'keyValuePairs'),
  6716. '#chatgpt_quota_surplus': createStructure('text', 'quota_surplus', false, 'dotSeparatedPath'),
  6717. '#chatgpt_quota_method': createStructure('text', 'quota_method', false),
  6718. };
  6719. let tempConfig_chatgpt = GM_getValue('chatgpt_config'); // 获取配置信息
  6720. const configManager_chatgpt = new ConfigManager('#chatgpt_config', 'chatgpt_config_', tempConfig_chatgpt, chatgptStructure, chatgptConfigEditHTML);
  6721. configManager_chatgpt.registerChoiceChange();
  6722.  
  6723. // Complet配置
  6724. const CompletStructure = {
  6725. '#name': createStructure('text', 'name', true),
  6726. '#complet_isChoose': createStructure('checkbox', 'isChoose', true),
  6727. '#complet_genre': createStructure('text', 'genre', true),
  6728. '#complet_language': createStructure('text', 'language', true),
  6729. '#complet_jsonUrl': createStructure('text', 'jsonUrl', true)
  6730. };
  6731. let tempConfig_Complet = GM_getValue('Complet_config'); // 获取配置信息
  6732. const configManager_complet = new ConfigManager('#Complet_config', 'complet_config_', tempConfig_Complet, CompletStructure, CompletConfigEditHTML, false);
  6733.  
  6734. // 状态更新
  6735. $("input[name='darkMode'][value='" + OJBetter.basic.darkMode + "']").prop("checked", true);
  6736. $("#showLoading").prop("checked", GM_getValue("showLoading") === true);
  6737. $("#expandFoldingblocks").prop("checked", GM_getValue("expandFoldingblocks") === true);
  6738. $("#renderPerfOpt").prop("checked", GM_getValue("renderPerfOpt") === true);
  6739. $("#selectElementPerfOpt").prop("checked", GM_getValue("selectElementPerfOpt") === true);
  6740. $("#commentPaging").prop("checked", GM_getValue("commentPaging") === true);
  6741. $("#standingsRecolor").prop("checked", GM_getValue("standingsRecolor") === true);
  6742. $("#hiddenProblemTag").prop("checked", GM_getValue("hiddenProblemTag") === true);
  6743. $("#showJumpToLuogu").prop("checked", GM_getValue("showJumpToLuogu") === true);
  6744. $("#showCF2vjudge").prop("checked", GM_getValue("showCF2vjudge") === true);
  6745. $("#hoverTargetAreaDisplay").prop("checked", GM_getValue("hoverTargetAreaDisplay") === true);
  6746. $("#showClistRating_contest").prop("checked", GM_getValue("showClistRating_contest") === true);
  6747. $("#showClistRating_problemset").prop("checked", GM_getValue("showClistRating_problemset") === true);
  6748. $("#showClistRating_problem").prop("checked", GM_getValue("showClistRating_problem") === true);
  6749. $("#RatingHidden").prop("checked", GM_getValue("RatingHidden") === true);
  6750. $('#scriptL10nLanguage').val(GM_getValue("scriptL10nLanguage"));
  6751. $('#localizationLanguage').val(GM_getValue("localizationLanguage"));
  6752. $("input[name='translation'][value='" + OJBetter.translation.choice + "']").prop("checked", true);
  6753. $("input[name='translation']").css("color", "gray");
  6754. $('#deepl_type').val(GM_getValue("deepl_type"));
  6755. $("#deepl_config_config_bar_ul").find(`input[name='deepl_config_config_item'][value='${tempConfig_deepl.choice}']`).prop("checked", true);
  6756. $('#enableEmphasisProtection').prop("checked", GM_getValue("enableEmphasisProtection") === true);
  6757. $('#enableLinkProtection').prop("checked", GM_getValue("enableLinkProtection") === true);
  6758. $("#chatgpt_config_config_bar_ul").find(`input[name='chatgpt_config_config_item'][value='${tempConfig_chatgpt.choice}']`).prop("checked", true);
  6759. $("#openai_isStream").prop("checked", GM_getValue("openai_isStream") === true);
  6760. $("#openai_asSystemPrompt").prop("checked", GM_getValue("openai_asSystemPrompt") === true);
  6761. $('#openai_customPrompt').val(GM_getValue("openai_customPrompt"));
  6762. $('#comment_translation_choice').val(GM_getValue("commentTranslationChoice"));
  6763. $('#iconButtonSize').val(GM_getValue("iconButtonSize"));
  6764. $("#autoTranslation").prop("checked", GM_getValue("autoTranslation") === true);
  6765. $('#shortTextLength').val(GM_getValue("shortTextLength"));
  6766. $("#allowMixTrans").prop("checked", GM_getValue("allowMixTrans") === true);
  6767. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6768. if (OJBetter.translation.auto.mixTrans.servers.indexOf($(this).val()) > -1) {
  6769. $(this).prop('checked', true);
  6770. }
  6771. });
  6772. // 翻译目标语言下拉框
  6773. $('#transTargetLang').change(function () {
  6774. var selectedLang = $(this).val();
  6775. updateRadioButtonsAvailability('#translationServices', selectedLang);
  6776. updateSelectOptionsAvailability('#comment_translation_choice', selectedLang);
  6777. updateCheckboxesAvailability('.OJBetter_checkboxs', selectedLang);
  6778. });
  6779. $('#transTargetLang').val(GM_getValue("transTargetLang"));
  6780. $('#transTargetLang').change();
  6781. //
  6782. $('#comment_translation_mode').val(GM_getValue("commentTranslationMode"));
  6783. $("#memoryTranslateHistory").prop("checked", GM_getValue("memoryTranslateHistory") === true);
  6784. $('#transWaitTime').val(GM_getValue("transWaitTime"));
  6785. $('#translation_replaceSymbol').val(GM_getValue("replaceSymbol"));
  6786. $("#filterTextWithoutEmphasis").prop("checked", GM_getValue("filterTextWithoutEmphasis") === true);
  6787. $("#forceTurndownConversion").prop("checked", GM_getValue("forceTurndownConversion") === true);
  6788. $('#translation_retransAction').val(GM_getValue("retransAction"));
  6789. $("#clist_Authorization").val(GM_getValue("clist_Authorization"));
  6790. $("#problemPageCodeEditor").prop("checked", GM_getValue("problemPageCodeEditor") === true);
  6791. $("#beautifyPreBlocks").prop("checked", GM_getValue("beautifyPreBlocks") === true);
  6792. $("#isCodeSubmitConfirm").prop("checked", GM_getValue("isCodeSubmitConfirm") === true);
  6793. $("#autoSubmitAfterPass").prop("checked", GM_getValue("autoSubmitAfterPass") === true);
  6794. $("#alwaysConsumeMouseWheel").prop("checked", GM_getValue("alwaysConsumeMouseWheel") === true);
  6795. $("#autoMemoryCode").prop("checked", GM_getValue("autoMemoryCode") === true);
  6796. $("#submitButtonPosition").val(GM_getValue("submitButtonPosition"));
  6797. $("#cppCodeTemplateComplete").prop("checked", GM_getValue("cppCodeTemplateComplete") === true);
  6798. $("#useLSP").prop("checked", GM_getValue("useLSP") === true);
  6799. $("#OJBetter_Bridge_WorkUri").val(GM_getValue("OJBetter_Bridge_WorkUri"));
  6800. $("#OJBetter_Bridge_SocketUrl").val(GM_getValue("OJBetter_Bridge_SocketUrl"));
  6801. $("input[name='compiler'][value='" + OJBetter.monaco.onlineCompilerChoice + "']").prop("checked", true);
  6802. $("input[name='compiler']").css("color", "gray");
  6803. // 调试
  6804. $("#notWaiteLoaded").prop("checked", GM_getValue("notWaiteLoaded") === true);
  6805. $('#l10n_refreshScrpitCacheButton').click(clearI18nextCache);
  6806. $('#l10n_web_refreshScrpitCacheButton').click(clearWebsiteL10nData);
  6807. $("#isRuleMarkingEnabled").prop("checked", GM_getValue("isRuleMarkingEnabled") === true);
  6808. $('#indexedDB_clearButton').click(async () => { await clearDatabase(); });
  6809. $('#indexedDB_exportButton').click(async () => { downloadDataAsFile(await exportDatabase(), 'database_export.json') });
  6810. $('#indexedDB_importButton').click(() => { readFileInput(async (fileContent) => { await importDatabase(fileContent); }); });
  6811. $('#configuration_clearButton').click(deleteAllConfigSettings);
  6812. $('#configuration_exportButton').click(() => { downloadDataAsFile(exportSettingsToJSON(), 'configuration_export.json') });
  6813. $('#configuration_importButton').click(() => { readFileInput((fileContent) => { importSettingsFromJSON(fileContent); }) });
  6814. // 关于
  6815. $('#updateChannel').val(GM_getValue("updateChannel"));
  6816. $('#updateSource').val(GM_getValue("updateSource"));
  6817. $('#updateChannel').change(function () {
  6818. var selectedLang = $(this).val();
  6819. updateUpdateSourceSelectOptionsAvailability('#updateSource', selectedLang);
  6820. if (selectedLang == "dev") $('#thanksforDevChannelNotice').show();
  6821. else $('#thanksforDevChannelNotice').hide();
  6822. });
  6823. $('#updateChannel').change();
  6824.  
  6825. // 关闭
  6826. const $settingMenu = $(".OJBetter_setting_menu");
  6827. $settingMenu.on("click", ".btn-close", async () => {
  6828. // 设置的数据
  6829. const settings = {
  6830. darkMode: $("input[name='darkMode']:checked").val(),
  6831. showLoading: $("#showLoading").prop("checked"),
  6832. hoverTargetAreaDisplay: $("#hoverTargetAreaDisplay").prop("checked"),
  6833. expandFoldingblocks: $("#expandFoldingblocks").prop("checked"),
  6834. renderPerfOpt: $("#renderPerfOpt").prop("checked"),
  6835. selectElementPerfOpt: $("#selectElementPerfOpt").prop("checked"),
  6836. commentPaging: $("#commentPaging").prop("checked"),
  6837. standingsRecolor: $("#standingsRecolor").prop("checked"),
  6838. hiddenProblemTag: $("#hiddenProblemTag").prop("checked"),
  6839. showJumpToLuogu: $("#showJumpToLuogu").prop("checked"),
  6840. showCF2vjudge: $("#showCF2vjudge").prop("checked"),
  6841. scriptL10nLanguage: $('#scriptL10nLanguage').val(),
  6842. localizationLanguage: $('#localizationLanguage').val(),
  6843. transTargetLang: $('#transTargetLang').val(),
  6844. translation: $("input[name='translation']:checked").val(),
  6845. deepl_type: $('#deepl_type').val(),
  6846. enableEmphasisProtection: $("#enableEmphasisProtection").prop("checked"),
  6847. enableLinkProtection: $("#enableLinkProtection").prop("checked"),
  6848. openai_isStream: $("#openai_isStream").prop("checked"),
  6849. openai_asSystemPrompt: $("#openai_asSystemPrompt").prop("checked"),
  6850. openai_customPrompt: $('#openai_customPrompt').val(),
  6851. commentTranslationChoice: $('#comment_translation_choice').val(),
  6852. iconButtonSize: $('#iconButtonSize').val(),
  6853. autoTranslation: $("#autoTranslation").prop("checked"),
  6854. shortTextLength: $('#shortTextLength').val(),
  6855. allowMixTrans: $("#allowMixTrans").prop("checked"),
  6856. mixedTranslation: (() => {
  6857. let mixedTranslation = [];
  6858. $('.OJBetter_checkboxs').find('input[type="checkbox"][name="mixedTranslation"]').each(function () {
  6859. if ($(this).is(":checked")) {
  6860. mixedTranslation.push($(this).val());
  6861. }
  6862. });
  6863. return mixedTranslation;
  6864. })(),
  6865. commentTranslationMode: $('#comment_translation_mode').val(),
  6866. memoryTranslateHistory: $('#memoryTranslateHistory').prop("checked"),
  6867. transWaitTime: $('#transWaitTime').val(),
  6868. replaceSymbol: $('#translation_replaceSymbol').val(),
  6869. filterTextWithoutEmphasis: $('#filterTextWithoutEmphasis').prop("checked"),
  6870. forceTurndownConversion: $('#forceTurndownConversion').prop("checked"),
  6871. retransAction: $('#translation_retransAction').val(),
  6872. showClistRating_contest: $('#showClistRating_contest').prop("checked"),
  6873. showClistRating_problemset: $('#showClistRating_problemset').prop("checked"),
  6874. showClistRating_problem: $('#showClistRating_problem').prop("checked"),
  6875. RatingHidden: $('#RatingHidden').prop("checked"),
  6876. clist_Authorization: $('#clist_Authorization').val(),
  6877. problemPageCodeEditor: $("#problemPageCodeEditor").prop("checked"),
  6878. beautifyPreBlocks: $("#beautifyPreBlocks").prop("checked"),
  6879. isCodeSubmitConfirm: $("#isCodeSubmitConfirm").prop("checked"),
  6880. autoSubmitAfterPass: $("#autoSubmitAfterPass").prop("checked"),
  6881. alwaysConsumeMouseWheel: $("#alwaysConsumeMouseWheel").prop("checked"),
  6882. autoMemoryCode: $("#autoMemoryCode").prop("checked"),
  6883. submitButtonPosition: $('#submitButtonPosition').val(),
  6884. cppCodeTemplateComplete: $("#cppCodeTemplateComplete").prop("checked"),
  6885. useLSP: $("#useLSP").prop("checked"),
  6886. OJBetter_Bridge_WorkUri: $('#OJBetter_Bridge_WorkUri').val().replace(/\\/g, '/').replace(/\/$/, ''),
  6887. OJBetter_Bridge_SocketUrl: $('#OJBetter_Bridge_SocketUrl').val(),
  6888. onlineCompilerChoice: $("input[name='compiler']:checked").val(),
  6889. notWaiteLoaded: $("#notWaiteLoaded").prop("checked"),
  6890. isRuleMarkingEnabled: $("#isRuleMarkingEnabled").prop("checked"),
  6891. updateChannel: $('#updateChannel').val(),
  6892. updateSource: $('#updateSource').val()
  6893. };
  6894. // tempConfigs的数据
  6895. const tempConfigs = {
  6896. 'deepl_config': configManager_deepl.getTempConfig(),
  6897. 'chatgpt_config': configManager_chatgpt.getTempConfig(),
  6898. 'Complet_config': configManager_complet.getTempConfig()
  6899. }
  6900.  
  6901. // 判断是否改变
  6902. let changes = {};
  6903. const combinedConfigs = Object.assign({}, settings, tempConfigs); // 合并settings和tempConfigs对象
  6904. for (const [key, value] of Object.entries(combinedConfigs)) {
  6905. const storedValue = GM_getValue(key);
  6906. if (!OJB_deepEquals(value, storedValue)) {
  6907. changes[key] = { oldValue: storedValue, newValue: value };
  6908. }
  6909. }
  6910.  
  6911. // 如果changes对象不为空,则有变化
  6912. if (Object.keys(changes).length > 0) {
  6913. console.log("Changes detected:", changes);
  6914. const shouldSave = await OJB_createDialog(
  6915. i18next.t('saveSetting.title', { ns: 'dialog' }),
  6916. i18next.t('saveSetting.content', { ns: 'dialog' }),
  6917. [
  6918. i18next.t('saveSetting.buttons.0', { ns: 'dialog' }),
  6919. i18next.t('saveSetting.buttons.1', { ns: 'dialog' })
  6920. ]
  6921. ); // 配置改变保存确认
  6922. if (shouldSave) {
  6923. // 数据校验
  6924. // TODO
  6925. if (settings.deepl_type !== 'free') {
  6926. let selectedIndex = $('input[name="deepl_config_config_item"]:checked').length > 0;
  6927. if (!selectedIndex) {
  6928. $('.deepl_config a').removeClass('active');
  6929. $('.settings-page').removeClass('active');
  6930. $('#sidebar-translation-settings').addClass('active');
  6931. $('#translation-settings').addClass('active');
  6932.  
  6933. $('#deepl_config').addClass('missing');
  6934. return;
  6935. } else {
  6936. $('#deepl_config').removeClass('missing');
  6937. }
  6938. }
  6939. if (settings.translation === "openai") {
  6940. let selectedIndex = $('input[name="chatgpt_config_config_item"]:checked').length > 0;
  6941. if (!selectedIndex) {
  6942. $('.chatgpt_config a').removeClass('active');
  6943. $('.settings-page').removeClass('active');
  6944. $('#sidebar-translation-settings').addClass('active');
  6945. $('#translation-settings').addClass('active');
  6946.  
  6947. $('#chatgpt_config').addClass('missing');
  6948. return;
  6949. } else {
  6950. $('#chatgpt_config').removeClass('missing');
  6951. }
  6952. }
  6953. {
  6954. let selectedIndex = $('input[name="translation"]:checked').length > 0;
  6955. if (!selectedIndex) {
  6956. $('.OJBetter_setting_sidebar a').removeClass('active');
  6957. $('.settings-page').removeClass('active');
  6958. $('#sidebar-translation-settings').addClass('active');
  6959. $('#translation-settings').addClass('active');
  6960.  
  6961. $('#translationServices').addClass('missing');
  6962. return;
  6963. } else {
  6964. $('#translationServices').removeClass('missing');
  6965. }
  6966. }
  6967.  
  6968. // 保存数据
  6969. let refreshPage = false; // 是否需要刷新页面
  6970. for (const [key, value] of Object.entries(settings)) {
  6971. if (!refreshPage && !(key == 'translation' || key == 'darkMode' || key == 'commentTranslationChoice')) {
  6972. if (GM_getValue(key) != value) refreshPage = true;
  6973. }
  6974. GM_setValue(key, value);
  6975. }
  6976. for (const [key, value] of Object.entries(tempConfigs)) {
  6977. if (!refreshPage && (JSON.stringify(GM_getValue(key)) != JSON.stringify(value))) refreshPage = true;
  6978. GM_setValue(key, value);
  6979. }
  6980.  
  6981. if (refreshPage) location.reload();
  6982. else {
  6983. // 切换黑暗模式
  6984. if (OJBetter.basic.darkMode != settings.darkMode) {
  6985. OJBetter.basic.darkMode = settings.darkMode;
  6986. // 移除旧的事件监听器
  6987. changeEventListeners.forEach(listener => {
  6988. mediaQueryList.removeEventListener('change', listener);
  6989. });
  6990.  
  6991. if (OJBetter.basic.darkMode == "follow") {
  6992. changeEventListeners.push(handleColorSchemeChange);
  6993. mediaQueryList.addEventListener('change', handleColorSchemeChange);
  6994. $('html').removeAttr('data-theme');
  6995. } else if (OJBetter.basic.darkMode == "dark") {
  6996. $('html').attr('data-theme', 'dark');
  6997. if (OJBetter.monaco.editor) {
  6998. monaco.editor.setTheme('vs-dark');
  6999. }
  7000. } else {
  7001. $('html').attr('data-theme', 'light');
  7002. if (OJBetter.monaco.editor) {
  7003. monaco.editor.setTheme('vs');
  7004. }
  7005. // 移除旧的事件监听器
  7006. const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
  7007. window.matchMedia('(prefers-color-scheme: dark)');
  7008. }
  7009. }
  7010. // 更新配置信息
  7011. OJBetter.translation.choice = settings.translation;
  7012. OJBetter.translation.comment.choice = settings.commentTranslationChoice;
  7013. }
  7014. }
  7015. }
  7016. OJB_closeAndRemoveModal(settingMenu);
  7017. $settingBtns.prop("disabled", false).removeClass("open");
  7018. });
  7019. });
  7020. };
  7021.  
  7022. /**
  7023. * 初始化html2markdown转换器
  7024. */
  7025. async function initHTML2MarkDown() {
  7026. OJBetter.common.turndownService = new TurndownService({ bulletListMarker: '-' });
  7027.  
  7028. // 保留原始
  7029. OJBetter.common.turndownService.keep(['del']);
  7030.  
  7031. // 丢弃
  7032. OJBetter.common.turndownService.addRule('remove-by-class', {
  7033. filter: function (node) {
  7034. return node.classList.contains('sample-tests') ||
  7035. node.classList.contains('header') ||
  7036. node.classList.contains('overlay') ||
  7037. node.classList.contains('html2md-panel') ||
  7038. node.classList.contains('likeForm') ||
  7039. node.classList.contains('monaco-editor') ||
  7040. node.nodeName === 'SCRIPT';
  7041. },
  7042. replacement: function (content, node) {
  7043. return "";
  7044. }
  7045. });
  7046. OJBetter.common.turndownService.addRule('remove-script', {
  7047. filter: function (node, options) {
  7048. return node.tagName.toLowerCase() == "script" && node.type.startsWith("math/tex");
  7049. },
  7050. replacement: function (content, node) {
  7051. return "";
  7052. }
  7053. });
  7054.  
  7055. // inline math
  7056. OJBetter.common.turndownService.addRule('inline-math', {
  7057. filter: function (node, options) {
  7058. return node.tagName.toLowerCase() == "span" && node.className == "MathJax";
  7059. },
  7060. replacement: function (content, node) {
  7061. var latex = $(node).next().text();
  7062. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  7063. return "$" + latex + "$";
  7064. }
  7065. });
  7066.  
  7067. // block math
  7068. OJBetter.common.turndownService.addRule('block-math', {
  7069. filter: function (node, options) {
  7070. return node.tagName.toLowerCase() == "div" && node.className == "MathJax_Display";
  7071. },
  7072. replacement: function (content, node) {
  7073. var latex = $(node).next().text();
  7074. latex = latex.replace(/</g, "&lt;").replace(/>/g, "&gt;");
  7075. return "\n$$\n" + latex + "\n$$\n";
  7076. }
  7077. });
  7078.  
  7079. // texFontStyle
  7080. OJBetter.common.turndownService.addRule('texFontStyle', {
  7081. filter: function (node) {
  7082. return (
  7083. node.nodeName === 'SPAN' &&
  7084. node.classList.contains('tex-font-style-bf')
  7085. )
  7086. },
  7087. replacement: function (content) {
  7088. return '**' + content + '**'
  7089. }
  7090. })
  7091.  
  7092. // sectionTitle
  7093. OJBetter.common.turndownService.addRule('sectionTitle', {
  7094. filter: function (node) {
  7095. return (
  7096. node.nodeName === 'DIV' &&
  7097. node.classList.contains('section-title')
  7098. )
  7099. },
  7100. replacement: function (content) {
  7101. return '**' + content + '**'
  7102. }
  7103. })
  7104.  
  7105. // property-title
  7106. OJBetter.common.turndownService.addRule('property-title', {
  7107. filter: function (node) {
  7108. return (
  7109. node.nodeName === 'DIV' &&
  7110. node.classList.contains('property-title')
  7111. )
  7112. },
  7113. replacement: function (content) {
  7114. return content + ': '
  7115. }
  7116. })
  7117.  
  7118. // pre
  7119. OJBetter.common.turndownService.addRule('pre', {
  7120. filter: function (node, options) {
  7121. return node.tagName.toLowerCase() == "pre";
  7122. },
  7123. replacement: function (content, node) {
  7124. if (!!node.querySelector('code.prettyprint')) {
  7125. return "";
  7126. } else {
  7127. return "```\n" + content + "```\n";
  7128. }
  7129. }
  7130. });
  7131.  
  7132. // bordertable
  7133. OJBetter.common.turndownService.addRule('bordertable', {
  7134. filter: 'table',
  7135. replacement: function (content, node) {
  7136. if (node.classList.contains('bordertable')) {
  7137. var output = [],
  7138. thead = '',
  7139. trs = node.querySelectorAll('tr');
  7140. if (trs.length > 0) {
  7141. var ths = trs[0].querySelectorAll('td,th');
  7142. if (ths.length > 0) {
  7143. thead = '| ' + Array.from(ths).map(th => OJBetter.common.turndownService.turndown(th.innerHTML.trim())).join(' | ') + ' |\n'
  7144. + '| ' + Array.from(ths).map(() => ' --- ').join('|') + ' |\n';
  7145. }
  7146. }
  7147. var rows = node.querySelectorAll('tr');
  7148. Array.from(rows).forEach(function (row, i) {
  7149. if (i > 0) {
  7150. var cells = row.querySelectorAll('td,th');
  7151. var trow = '| ' + Array.from(cells).map(cell => OJBetter.common.turndownService.turndown(cell.innerHTML.trim())).join(' | ') + ' |';
  7152. output.push(trow);
  7153. }
  7154. });
  7155. return thead + output.join('\n');
  7156. } else {
  7157. return content;
  7158. }
  7159. }
  7160. });
  7161. };
  7162.  
  7163. /**
  7164. * 任务队列
  7165. */
  7166. class TaskQueue {
  7167. constructor() {
  7168. this.taskQueues = {};
  7169. this.isProcessing = {}; // 处理状态
  7170. this.delays = {}; // 等待时间(毫秒)
  7171. }
  7172.  
  7173. getDelay(type) {
  7174. if (type === 'openai') {
  7175. return 0;
  7176. } else {
  7177. return OJBetter.translation.waitTime;
  7178. }
  7179. }
  7180.  
  7181. /**
  7182. * 添加任务
  7183. * @param {string} type 任务类型
  7184. * @param {function} fn 任务函数
  7185. * @param {boolean} isNonQueueTask 是否为非队列任务
  7186. */
  7187. addTask(type, fn, isNonQueueTask = false) {
  7188. if (!this.taskQueues[type]) {
  7189. this.taskQueues[type] = [];
  7190. }
  7191.  
  7192. if (isNonQueueTask) {
  7193. fn();
  7194. } else {
  7195. this.taskQueues[type].push(fn);
  7196.  
  7197. if (!this.isProcessing[type]) {
  7198. this.processQueue(type);
  7199. }
  7200. }
  7201. }
  7202.  
  7203. async processQueue(type) {
  7204. this.isProcessing[type] = true;
  7205.  
  7206. while (this.taskQueues[type].length > 0) {
  7207. const task = this.taskQueues[type].shift();
  7208. await task();
  7209.  
  7210. if (this.taskQueues[type].length > 0) {
  7211. await this.wait(this.getDelay(type));
  7212. }
  7213. }
  7214.  
  7215. this.isProcessing[type] = false;
  7216. }
  7217.  
  7218. wait(delay) {
  7219. return new Promise(resolve => {
  7220. setTimeout(resolve, delay);
  7221. });
  7222. }
  7223. }
  7224.  
  7225. /**
  7226. * 检测为空文本
  7227. * @param {string} text 待检测的文本
  7228. * @returns {boolean} 是否为空文本
  7229. */
  7230. const isEmptyText = text => text.trim() === '';
  7231.  
  7232. /**
  7233. * 加载按钮相关函数
  7234. */
  7235. async function initButtonFunc() {
  7236. // 鼠标悬浮时为目标元素区域添加一个覆盖层
  7237. $.fn.addHoverOverlay = function (target) {
  7238. let position = $(target).css('position');
  7239. let display = $(target).css('display');
  7240.  
  7241. this.hover(() => {
  7242. $(target)
  7243. .addClass('overlay')
  7244. .css('position', 'relative');
  7245. if (display == "inline" || display == "contents") {
  7246. $(target).css('display', 'block');
  7247. }
  7248. }, () => {
  7249. $(target)
  7250. .removeClass('overlay')
  7251. .css('position', position);
  7252. if (display == "inline" || display == "contents") {
  7253. $(target).css('display', display);
  7254. }
  7255. })
  7256. }
  7257.  
  7258. /**
  7259. * 为按钮设置图标
  7260. * @param {string} icon 图标
  7261. * @returns {JQuery<HTMLElement>} 按钮
  7262. */
  7263. $.fn.setButtonIcon = function (icon) {
  7264. let i = this.find("i");
  7265. if (i.length != 0 && i.hasClass("iconfont")) {
  7266. i.html(icon);
  7267. } else {
  7268. i = OJB_safeCreateJQElement(`<i>${icon}</i>`);
  7269. this.prepend(i);
  7270. }
  7271. return this;
  7272. }
  7273.  
  7274. /**
  7275. * 设置按钮为加载等待状态
  7276. */
  7277. $.fn.setButtonLoading = function () {
  7278. this.addClass("loading");
  7279. this.prop("disabled", true);
  7280. return this;
  7281. }
  7282.  
  7283. /**
  7284. * 解除按钮的加载等待状态
  7285. */
  7286. $.fn.setButtonLoaded = function () {
  7287. this.removeClass("loading");
  7288. this.prop("disabled", false);
  7289. return this;
  7290. }
  7291.  
  7292. /**
  7293. * 为按钮设置popover提示文本
  7294. * @param {string} text 文本
  7295. * @returns {JQuery<HTMLElement>} 按钮
  7296. */
  7297. $.fn.setButtonPopover = function (text) {
  7298. // find if has popover_content class element
  7299. let popover_content = this.find(".popover_content");
  7300. if (popover_content.length != 0) {
  7301. popover_content.text(text);
  7302. } else {
  7303. popover_content = OJB_safeCreateJQElement(`<span class="popover_content">${text}</span>`);
  7304. this.append(popover_content);
  7305. }
  7306. return this;
  7307. }
  7308.  
  7309. /**
  7310. * 获取MarkDown
  7311. * @returns {string} MarkDown
  7312. */
  7313. $.fn.getMarkdown = function () {
  7314. const markdown = this.data('markdown');
  7315. if (markdown === undefined) {
  7316. const htmlContent = this.html();
  7317. const newMarkdown = OJBetter.common.turndownService.turndown(htmlContent);
  7318. this.data('markdown', newMarkdown);
  7319. return newMarkdown;
  7320. }
  7321. return markdown;
  7322. }
  7323.  
  7324. // 设置按钮状态
  7325. $.fn.setButtonState = function (state, popoverText = null, disabled = false) {
  7326. this.data('buttonState', state)
  7327. .prop('disabled', disabled)
  7328. .css('cursor', disabled ? 'not-allowed' : 'pointer')
  7329. .removeClass('running success enabled error loading redo');
  7330. if (popoverText) this.setButtonPopover(popoverText);
  7331.  
  7332. if (state !== 'initial') this.addClass(state);
  7333. return this;
  7334. };
  7335.  
  7336. // 为按钮添加鼠标悬浮重试
  7337. $.fn.setHoverRedo = function () {
  7338. this.hover(() => {
  7339. prevState = this.getButtonState();
  7340. if (prevState !== "normal" && prevState !== "running") {
  7341. this.setButtonState('redo');
  7342. }
  7343. }, () => {
  7344. const currentState = this.getButtonState();
  7345. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7346. this.setButtonState(prevState);
  7347. prevState = null;
  7348. }
  7349. });
  7350. };
  7351.  
  7352. // 获取按钮状态
  7353. $.fn.getButtonState = function () {
  7354. return this.data('buttonState') || 'normal';
  7355. };
  7356.  
  7357. // 设置翻译按钮状态
  7358. $.fn.setTransButtonState = function (state, text = null) {
  7359. const popoverText = text || i18next.t(`trans.${state}`, { ns: 'button' });
  7360. const disabled = state === 'running' || state === 'loading';
  7361. this.setButtonState(state, popoverText, disabled);
  7362. return this;
  7363. };
  7364.  
  7365. // 存翻译结果
  7366. $.fn.pushResultToTransButton = function (result) {
  7367. let resultStack = this.data('resultStack');
  7368. if (!resultStack) resultStack = [];
  7369. resultStack.push(result);
  7370. this.data('resultStack', resultStack);
  7371. }
  7372.  
  7373. // 获取翻译结果
  7374. $.fn.getResultFromTransButton = function () {
  7375. return this.data('resultStack');
  7376. }
  7377.  
  7378. // 标记为不自动翻译
  7379. $.fn.setNotAutoTranslate = function () {
  7380. this.data('notAutoTranslate', true);
  7381. }
  7382.  
  7383. // 获取是否为不自动翻译
  7384. $.fn.getNotAutoTranslate = function () {
  7385. return this.data('notAutoTranslate');
  7386. }
  7387.  
  7388. // 判断是否已经翻译
  7389. $.fn.IsTranslated = function () {
  7390. if (this.hasAttr('translated')) {
  7391. return true;
  7392. } else {
  7393. return false;
  7394. }
  7395. }
  7396.  
  7397. // 判断是否为评论区按钮
  7398. $.fn.IsCommentButton = function () {
  7399. let isCommentButton = this.data('isCommentButton');
  7400. if (isCommentButton == undefined) {
  7401. this.parents('.comments').length > 0 ? isCommentButton = true : isCommentButton = false;
  7402. this.data('isCommentButton', isCommentButton);
  7403. }
  7404. return isCommentButton;
  7405. }
  7406.  
  7407. // 按钮点击效果
  7408. $(document).on('mousedown', '.ojb_btn', function () {
  7409. $(this).addClass('active').on('animationend', () => $(this).removeClass('active'));
  7410. });
  7411. }
  7412.  
  7413. /**
  7414. * 添加题目markdown转换/复制/翻译按钮面板
  7415. * @param {HTMLElement} element 需要添加按钮面板的元素
  7416. * @param {string} suffix 按钮面板id后缀
  7417. * @param {string} type 按钮面板添加位置
  7418. * @param {boolean} is_simple 是否是简单模式
  7419. * @returns {object} 返回按钮面板元素
  7420. */
  7421. function addButtonPanel(element, suffix, type, is_simple = false) {
  7422. let text;
  7423. if (OJBetter.translation.comment.transMode == "1") text = i18next.t('trans.segment', { ns: 'button' });
  7424. else if (OJBetter.translation.comment.transMode == "2") text = i18next.t('trans.select', { ns: 'button' });
  7425. else text = i18next.t('trans.normal', { ns: 'button' });
  7426.  
  7427. let panel = OJB_safeCreateJQElement(`<div class='html2md-panel input-output-copier ${is_simple ? 'is_simple' : ''}'></div>`);
  7428. let viewButton = OJB_safeCreateJQElement(`
  7429. <button class='ojb_btn ojb_btn_popover top' id='html2md-view${suffix}'>
  7430. <i class="iconfont">&#xe7e5;</i>
  7431. <span class="popover_content">${i18next.t('md.normal', { ns: 'button' })}</span>
  7432. </button>`);
  7433. let copyButton = OJB_safeCreateJQElement(`
  7434. <button class='ojb_btn ojb_btn_popover top' id='html2md-cb${suffix}'>
  7435. <i class="iconfont">&#xe608;</i>
  7436. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  7437. </button>`);
  7438. let translateButton = OJB_safeCreateJQElement(`
  7439. <button class='ojb_btn translateButton ojb_btn_popover top' id='translateButton${suffix}'>
  7440. <i class="iconfont">&#xe6be;</i>
  7441. <span class="popover_content">${text}</span>
  7442. </button>`);
  7443. if (!is_simple) panel.append(viewButton);
  7444. if (!is_simple) panel.append(copyButton);
  7445. panel.append(translateButton);
  7446. if (type === "this_level") {
  7447. $(element).before(panel);
  7448. } else if (type === "child_level") {
  7449. $(element).prepend(panel);
  7450. }
  7451.  
  7452. return {
  7453. panel: panel,
  7454. viewButton: viewButton,
  7455. copyButton: copyButton,
  7456. translateButton: translateButton
  7457. }
  7458. }
  7459.  
  7460. /**
  7461. * 添加MD视图按钮
  7462. * @param {JQuery<HTMLElement>} button 按钮
  7463. * @param {JQuery<HTMLElement>} element 目标元素
  7464. * @param {string} suffix id后缀
  7465. * @param {string} type 类型
  7466. * @returns {void}
  7467. */
  7468. async function addButtonWithHTML2MD(button, element, suffix, type) {
  7469. /**
  7470. * 改变按钮状态
  7471. * @param {string} state 状态
  7472. */
  7473. const changeButtonState = (state) => {
  7474. if (state == "loading") {
  7475. button.setButtonLoading();
  7476. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7477. } else if (state == "loaded") {
  7478. button.setButtonLoaded();
  7479. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7480. } else if (state == "normal") {
  7481. button.removeClass("enabled");
  7482. button.setButtonPopover(i18next.t('md.normal', { ns: 'button' }));
  7483. } else if (state == "mdView") {
  7484. button.addClass("enabled");
  7485. button.setButtonPopover(i18next.t('md.reduction', { ns: 'button' }));
  7486. } else if (state == "disabled") {
  7487. button.prop("disabled", true);
  7488. button.setButtonPopover(i18next.t('md.disabled', { ns: 'button' }));
  7489. }
  7490. }
  7491.  
  7492. /**
  7493. * 存放目标元素的 JQueryObject
  7494. */
  7495. const target = (() => {
  7496. if (type = "child_level") {
  7497. return $(element).children(':not(.html2md-panel)');
  7498. } else {
  7499. return $(element);
  7500. }
  7501. })();
  7502.  
  7503. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7504. changeButtonState("disabled");
  7505. return;
  7506. } else {
  7507. changeButtonState("loading");
  7508. await waitForMathJaxIdle();
  7509. changeButtonState("loaded");
  7510. }
  7511.  
  7512. button.click(OJB_debounce(function () {
  7513. /**
  7514. * 检查是否是MarkDown视图
  7515. * @returns {boolean} 是否是MarkDown视图
  7516. */
  7517. function checkViewmd() {
  7518. if ($(element).attr("viewmd") === "true") {
  7519. return true;
  7520. } else {
  7521. return false;
  7522. }
  7523. }
  7524.  
  7525. /**
  7526. * 设置是否是MarkDown视图
  7527. * @param {boolean} value 是否是MarkDown视图
  7528. * @returns {void}
  7529. */
  7530. function setViewmd(value) {
  7531. $(element).attr("viewmd", value);
  7532. if (value) {
  7533. changeButtonState("mdView");
  7534. } else {
  7535. changeButtonState("normal");
  7536. }
  7537. }
  7538.  
  7539. if (checkViewmd()) {
  7540. setViewmd(false);
  7541. target.last().next(".mdViewContent").remove();
  7542. target.show();
  7543. } else {
  7544. setViewmd(true);
  7545. const markdown = $(element).getMarkdown();
  7546. const mdViewContent = OJB_safeCreateJQElement(`<span class="mdViewContent" style="width:auto; height:auto;">${markdown}</span>`);
  7547. target.last().after(mdViewContent);
  7548. target.hide();
  7549. }
  7550. }));
  7551.  
  7552. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7553. button.addHoverOverlay($(element));
  7554. }
  7555. }
  7556.  
  7557. /**
  7558. * 添加复制按钮
  7559. * @param {JQuery<HTMLElement>} button 按钮
  7560. * @param {JQuery<HTMLElement>} element 目标元素
  7561. * @param {string} suffix 后缀
  7562. * @param {string} type 类型
  7563. */
  7564. async function addButtonWithCopy(button, element, suffix, type) {
  7565. /**
  7566. * 改变按钮状态
  7567. * @param {string} state 状态
  7568. */
  7569. function changeButtonState(state) {
  7570. if (state == "loading") {
  7571. button.setButtonLoading();
  7572. button.setButtonPopover(i18next.t('state.waitMathJax', { ns: 'button' }));
  7573. } else if (state == "loaded") {
  7574. button.setButtonLoaded();
  7575. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7576. } else if (state == "normal") {
  7577. button.setButtonPopover(i18next.t('copy.normal', { ns: 'button' }));
  7578. } else if (state == "copied") {
  7579. button.setButtonPopover(i18next.t('copy.copied', { ns: 'button' }));
  7580. } else if (state == "disabled") {
  7581. button.prop("disabled", true);
  7582. button.setButtonPopover(i18next.t('copy.disabled', { ns: 'button' }));
  7583. }
  7584. }
  7585.  
  7586. // 等待MathJax队列完成
  7587. if (OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) {
  7588. changeButtonState("disabled");
  7589. return;
  7590. } else {
  7591. changeButtonState("loading");
  7592. await waitForMathJaxIdle();
  7593. changeButtonState("loaded");
  7594. }
  7595.  
  7596. button.click(OJB_debounce(function () {
  7597. var target = $(element).get(0);
  7598.  
  7599. var markdown = $(element).getMarkdown();
  7600.  
  7601. GM_setClipboard(markdown);
  7602.  
  7603. $(this).addClass("success");
  7604. changeButtonState("copied");
  7605.  
  7606.  
  7607. // 更新复制按钮文本
  7608. setTimeout(() => {
  7609. $(this).removeClass("success");
  7610. changeButtonState("normal")
  7611. }, 2000);
  7612. }));
  7613.  
  7614. if (OJBetter.preference.hoverTargetAreaDisplay && !OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  7615. button.addHoverOverlay($(element));
  7616. }
  7617. }
  7618.  
  7619. /**
  7620. * 添加翻译按钮
  7621. * @param {JQuery<HTMLElement>} button 按钮
  7622. * @param {JQuery<HTMLElement>} element 目标元素
  7623. * @param {string} suffix 后缀
  7624. * @param {string} type 类型
  7625. * @param {boolean} is_comment 是否是评论
  7626. */
  7627. async function addButtonWithTranslation(button, element, suffix, type, is_comment = false) {
  7628. /**
  7629. * 添加可指定翻译服务的方法调用
  7630. * @param {string} translation 翻译服务
  7631. */
  7632. button.data("translatedItBy", function (translation) {
  7633. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7634. OJBetter.common.taskQueue.addTask(translation, () => transTask(button, element, type, is_comment, translation), translation == 'openai');
  7635. });
  7636.  
  7637. // 等待MathJax队列完成
  7638. button.setButtonLoading();
  7639. await waitForMathJaxIdle();
  7640. button.setButtonLoaded();
  7641.  
  7642. // 标记目标文本区域不自动翻译
  7643. {
  7644. let text;
  7645. if ((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion)) {
  7646. text = $(element).html();
  7647. } else {
  7648. text = $(element).getMarkdown();
  7649. }
  7650. let length = text.length;
  7651. if (length > OJBetter.translation.auto.shortTextLength || isEmptyText(text) || $(element).find('.spoiler').length > 0) {
  7652. button.setNotAutoTranslate();
  7653. }
  7654. // button.after(`<span>${length}</span>`); // 显示字符数
  7655. }
  7656.  
  7657. button.click(OJB_debounce(async function () {
  7658. // 重新翻译
  7659. let resultStack = $(this).getResultFromTransButton();
  7660. if (resultStack) {
  7661. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected");
  7662. for (let item of resultStack) {
  7663. if (OJBetter.translation.retransAction == "0") {
  7664. // 选段翻译不直接移除旧结果
  7665. if (OJBetter.translation.comment.transMode == "2") {
  7666. // 只移除即将要翻译的段的结果
  7667. if (pElements.is(item.translateDiv.getDiv().prev())) {
  7668. item.translateDiv.close();
  7669. }
  7670. } else {
  7671. item.translateDiv.close();
  7672. $($(element)).find(".translate-problem-statement, .translate-problem-statement-panel").remove();
  7673. }
  7674. } else {
  7675. item.translateDiv.fold();
  7676. }
  7677. }
  7678. }
  7679.  
  7680. // 翻译
  7681. button.setTransButtonState('running', i18next.t('trans.wait', { ns: 'button' }));
  7682. OJBetter.common.taskQueue.addTask(OJBetter.translation.choice, () => transTask(button, element, type, is_comment), OJBetter.translation.choice == 'openai');
  7683. }));
  7684.  
  7685. // 重新翻译提示
  7686. let prevState;
  7687. button.hover(() => {
  7688. prevState = button.getButtonState();
  7689. if (prevState !== "normal" && prevState !== "running") {
  7690. button.setTransButtonState('redo');
  7691. }
  7692. }, () => {
  7693. const currentState = button.getButtonState();
  7694. if (prevState !== "normal" && ["normal", "redo"].includes(currentState)) {
  7695. button.setTransButtonState(prevState);
  7696. prevState = null;
  7697. }
  7698. });
  7699.  
  7700. // 目标区域指示
  7701. if (OJBetter.preference.hoverTargetAreaDisplay) {
  7702. button.addHoverOverlay($(element));
  7703. }
  7704.  
  7705. // 翻译右键切换菜单
  7706. $(document).on('contextmenu', '#translateButton' + suffix, function (e) {
  7707. e.preventDefault();
  7708.  
  7709. // 是否为评论的翻译
  7710. let is_comment = button.IsCommentButton();
  7711.  
  7712. // 移除旧的
  7713. if (!$(e.target).closest('.OJBetter_contextmenu').length) {
  7714. $('.OJBetter_contextmenu').remove();
  7715. }
  7716.  
  7717. var menu = $('<div class="OJBetter_contextmenu"></div>');
  7718. var translations = [
  7719. { value: 'deepl', name: i18next.t('translation.options.services.deepl', { ns: 'settings' }) },
  7720. { value: 'iflyrec', name: i18next.t('translation.options.services.iflyrec', { ns: 'settings' }) },
  7721. { value: 'youdao', name: i18next.t('translation.options.services.youdao', { ns: 'settings' }) },
  7722. { value: 'google', name: i18next.t('translation.options.services.google', { ns: 'settings' }) },
  7723. { value: 'caiyun', name: i18next.t('translation.options.services.caiyun', { ns: 'settings' }) },
  7724. { value: 'openai', name: i18next.t('translation.options.services.openai.name', { ns: 'settings' }) }
  7725. ];
  7726.  
  7727. // Function to check if the service supports the target language
  7728. function supportsTargetLanguage(service, targetLang) {
  7729. return OJBetter.supportList.translationSupport[service] && OJBetter.supportList.translationSupport[service][targetLang] !== undefined;
  7730. }
  7731.  
  7732. if (is_comment) {
  7733. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="0">
  7734. <span class="OJBetter_contextmenu_label_text">
  7735. ${i18next.t('translation.preference.comment_translation_choice.services.follow', { ns: 'settings' })}
  7736. </span></label>`);
  7737. menu.append(label);
  7738. }
  7739. translations.forEach(function (translation) {
  7740. if (supportsTargetLanguage(translation.value, OJBetter.translation.targetLang)) {
  7741. var label = OJB_safeCreateJQElement(`<label><input type="radio" name="translation" value="${translation.value}">
  7742. <span class="OJBetter_contextmenu_label_text">${translation.name}</span></label>`);
  7743. menu.append(label);
  7744. }
  7745. });
  7746.  
  7747. // 初始化
  7748. if (is_comment) {
  7749. menu.find(`input[name="translation"][value="${OJBetter.translation.comment.choice}"]`).prop('checked', true);
  7750. } else {
  7751. menu.find(`input[name="translation"][value="${OJBetter.translation.choice}"]`).prop('checked', true);
  7752. }
  7753. menu.css({
  7754. top: e.pageY + 'px',
  7755. left: e.pageX + 'px'
  7756. }).appendTo('body');
  7757.  
  7758. $(document).one('change', 'input[name="translation"]', function () {
  7759. if (is_comment) {
  7760. OJBetter.translation.comment.choice = $('input[name="translation"]:checked').val();
  7761. GM_setValue("commentTranslationChoice", OJBetter.translation.comment.choice);
  7762. } else {
  7763. OJBetter.translation.choice = $('input[name="translation"]:checked').val();
  7764. GM_setValue("translation", OJBetter.translation.choice);
  7765. }
  7766. $('.OJBetter_contextmenu').remove();
  7767. });
  7768.  
  7769. // 点击区域外关闭菜单
  7770. function handleClick(event) {
  7771. if (!$(event.target).closest('.OJBetter_contextmenu').length) {
  7772. $('.OJBetter_contextmenu').remove();
  7773. $(document).off('change', 'input[name="translation"]');
  7774. } else {
  7775. $(document).one('click', handleClick);
  7776. }
  7777. }
  7778. $(document).one('click', handleClick);
  7779. });
  7780. }
  7781.  
  7782. /**
  7783. * 创建翻译任务
  7784. * @param {JQuery<HTMLElement>} button 按钮
  7785. * @param {HTMLElement} element 目标元素
  7786. * @param {string} type 类型
  7787. * @param {boolean} is_comment 是否是评论
  7788. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7789. */
  7790. async function transTask(button, element, type, is_comment, overrideTrans) {
  7791. /** @type {HTMLElement} 目标元素 */
  7792. let target;
  7793. /**
  7794. * 错误计数数据结构
  7795. * @typedef {Object} count
  7796. * @property {number} errerNum 错误数量
  7797. * @property {number} skipNum 跳过数量
  7798. */
  7799. const count = {
  7800. errerNum: 0,
  7801. skipNum: 0
  7802. };
  7803. if (OJBetter.translation.comment.transMode == "1") {
  7804. // 分段翻译
  7805. let pElements = $(element).find("p:not(li p), li, .OJBetter_acmsguru");
  7806. for (let i = 0; i < pElements.length; i++) {
  7807. target = $(pElements[i]).eq(0).clone();
  7808. element_node = pElements[i];
  7809. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7810. }
  7811. } else if (OJBetter.translation.comment.transMode == "2") {
  7812. // 选段翻译
  7813. let pElements = $(element).find("p.block_selected:not(li p), li.block_selected, .OJBetter_acmsguru");
  7814. for (let i = 0; i < pElements.length; i++) {
  7815. target = $(pElements[i]).eq(0).clone();
  7816. element_node = pElements[i];
  7817. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7818. }
  7819. $(element).find("p.block_selected:not(li p), li.block_selected").removeClass('block_selected');
  7820. } else {
  7821. // 普通翻译
  7822. target = $(element).eq(0).clone();
  7823. if (type === "child_level") $(target).children(':first').remove();
  7824. element_node = $($(element)).get(0);
  7825. await process(button, target, element_node, type, is_comment, count, overrideTrans);
  7826. }
  7827.  
  7828. // 翻译完成
  7829. if (!count.errerNum && !count.skipNum) {
  7830. button.setTransButtonState('success');
  7831. }
  7832. }
  7833.  
  7834. /**
  7835. * 翻译处理
  7836. * @param {JQuery<HTMLElement>} button 按钮
  7837. * @param {HTMLElement} target 目标元素
  7838. * @param {HTMLElement} element_node 目标节点
  7839. * @param {string} type 类型
  7840. * @param {boolean} is_comment 是否是评论
  7841. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7842. */
  7843. async function process(button, target, element_node, type, is_comment, count, overrideTrans) {
  7844. if (type === "child_level") {
  7845. let div = $("<div>");
  7846. $(element_node).append(div);
  7847. element_node = div.get(0);
  7848. }
  7849.  
  7850. //是否跳过折叠块
  7851. if ($(target).find('.spoiler').length > 0) {
  7852. const shouldSkip = await OJB_createDialog(
  7853. i18next.t('skipFold.title', { ns: 'dialog' }),
  7854. i18next.t('skipFold.content', { ns: 'dialog' }),
  7855. [
  7856. i18next.t('skipFold.buttons.0', { ns: 'dialog' }),
  7857. i18next.t('skipFold.buttons.1', { ns: 'dialog' })
  7858. ],
  7859. true
  7860. ); //跳过折叠块确认
  7861. if (shouldSkip) {
  7862. $(target).find('.spoiler').remove();
  7863. } else {
  7864. $(target).find('.html2md-panel').remove();
  7865. }
  7866. }
  7867.  
  7868. // 等待并获取结果
  7869. button.setTransButtonState('running');
  7870. const result = await blockProcessing(button, target, element_node, is_comment, overrideTrans);
  7871. button.pushResultToTransButton(result);
  7872.  
  7873. if (result.status == "error") count.errerNum += 1;
  7874. else if (result.status == "skip") count.skipNum += 1;
  7875. $(target).remove();
  7876. }
  7877.  
  7878. /**
  7879. * 块处理
  7880. * @param {JQuery<HTMLElement>} button
  7881. * @param {HTMLElement} target 目标元素
  7882. * @param {HTMLElement} element_node 目标节点
  7883. * @param {boolean} is_comment 是否是评论
  7884. * @param {string} overrideTrans 覆盖全局翻译服务设定
  7885. * @returns {TranslateResult} 翻译结果对象
  7886. */
  7887. async function blockProcessing(button, target, element_node, is_comment, overrideTrans) {
  7888. if ((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion)) {
  7889. target.markdown = $(target).html();
  7890. } else if (!target.markdown) {
  7891. target.markdown = OJBetter.common.turndownService.turndown($(target).html());
  7892. }
  7893.  
  7894. const result = await translateProblemStatement(target.markdown, element_node, is_comment, overrideTrans);
  7895. if (result.status == "skip") {
  7896. button.setTransButtonState('error', i18next.t('trans.tooLong', { ns: 'button' }));
  7897. result.translateDiv.close();
  7898. } else if (result.status == "error" || !result.rawData.done) {
  7899. result.translateDiv.setError();
  7900. result.translateDiv.setRawData(result.rawData);
  7901. result.translateDiv.showDebugButton();
  7902. button.setTransButtonState('error', i18next.t('trans.error', { ns: 'button' }));
  7903. $(target).remove();
  7904. }
  7905. return result;
  7906. }
  7907.  
  7908. /**
  7909. * 选段翻译支持
  7910. */
  7911. async function multiChoiceTranslation() {
  7912. GM_addStyle(`
  7913. .topic .content .ttypography {
  7914. overflow: initial;
  7915. }
  7916. `);
  7917.  
  7918. $(document).on('click', 'p, li:not(:has(.comment)), .OJBetter_acmsguru', function (e) {
  7919. let $this = $(this);
  7920. e.stopPropagation();
  7921. if ($this.hasClass('block_selected')) {
  7922. $this.removeClass('block_selected');
  7923. // 移除对应的按钮
  7924. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + $this.attr('OJBetter_p_id'));
  7925. } else {
  7926. let id = OJB_getRandomNumber(8);
  7927. $this.attr('OJBetter_p_id', id);
  7928. $this.addClass('block_selected');
  7929. // 添加按钮
  7930. let menu = OJB_safeCreateJQElement(`<div class="OJBetter_MiniTranslateButton" id='translateButton_selected_${id}'>${translateIcon}</div>`)
  7931. .css({
  7932. left: $($this).outerWidth(true) + $($this).position().left + 10 + 'px',
  7933. });
  7934. $this.before(menu);
  7935.  
  7936. $("#translateButton_selected_" + id).click(async function () {
  7937. // 处理旧的结果
  7938. if ($this.attr('translated')) {
  7939. let result = $this.data("resultData");
  7940. if (OJBetter.translation.retransAction == "0") {
  7941. result.translateDiv.close();
  7942. } else {
  7943. result.translateDiv.fold();
  7944. }
  7945. }
  7946. // 翻译
  7947. let target = $this.eq(0).clone();
  7948. let result = await blockProcessing(OJBetter.translation.choice, target, $this.eq(0), $("#translateButton_selected_" + id), false);
  7949. $this.data("resultData", result);
  7950. $this.removeClass('block_selected');
  7951. // 移除对应的按钮
  7952. $('.OJBetter_MiniTranslateButton').remove("#translateButton_selected_" + id);
  7953. $this.attr('translated', '1'); // 标记已翻译
  7954. });
  7955. }
  7956. });
  7957. }
  7958.  
  7959. /**
  7960. * 为acmsguru题面重新划分div
  7961. */
  7962. async function acmsguruReblock() {
  7963. if (OJBetter.translation.comment.transMode == '0') {
  7964. // 普通模式下的划分方式
  7965. var html = $('.ttypography').children().html();
  7966. var separator = /(<div align="left" style="margin-top: 1\.0em;"><b>.*?<\/b><\/div>)/g;
  7967. var result = html.split(separator); // 分割代码
  7968. var outputHtml = '';
  7969. var header = '';
  7970. for (var i = 0; i < result.length; i++) {
  7971. if (separator.test(result[i])) {
  7972. header = result[i];
  7973. continue;
  7974. }
  7975. outputHtml += '<div class="ttypography">' + header + result[i] + '</div>';
  7976. header = '';
  7977. }
  7978. $('.ttypography').html(outputHtml);
  7979. }
  7980. else {
  7981. // 分段/选段模式下的划分方式
  7982. $('.ttypography').children().each(function () {
  7983. var html = $(this).html();
  7984. var replacedHtml = html.replace(/(?:<\/div>|<br><br>)(?<text>[\s\S]+?)(?=<br><br>)/g,
  7985. '<div align="left" class="OJBetter_acmsguru" >$<text></div>');
  7986. $(this).html(replacedHtml);
  7987. });
  7988. }
  7989. }
  7990.  
  7991. /**
  7992. * 添加MD/复制/翻译按钮
  7993. */
  7994. async function addConversionButton() {
  7995. let promises = []; // 用于收集所有的 Promise
  7996.  
  7997. // 题目页添加按钮
  7998. if (OJBetter.typeOfPage.is_problem) {
  7999. let exContentsPageClasses = ["sample-tests"];
  8000. $('.problem-statement').children('div').each((i, e) => {
  8001. var className = $(e).attr('class');
  8002. if (!exContentsPageClasses.includes(className)) {
  8003. var id = "_problem_" + OJB_getRandomNumber(8);
  8004. let panel = addButtonPanel(e, id, "this_level");
  8005. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  8006. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  8007. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  8008. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  8009. }
  8010. });
  8011. }
  8012. // 添加按钮到ttypography部分
  8013. $(".ttypography").each((i, e) => {
  8014. // 是否为评论
  8015. let is_comment = false;
  8016. if ($(e).parents('.comments').length > 0) is_comment = true;
  8017. // 题目页不添加
  8018. if (!OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_acmsguru) {
  8019. let id = "_ttypography_" + OJB_getRandomNumber(8);
  8020. let panel = addButtonPanel(e, id, "this_level");
  8021. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  8022. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  8023. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level", is_comment));
  8024. }
  8025. });
  8026.  
  8027. // 完整题目集页特殊处理
  8028. if (OJBetter.typeOfPage.is_completeProblemset) {
  8029. let exContentsPageClasses = ["sample-tests"];
  8030. $('.problem-statement').each(function () {
  8031. $(this).children('div').each((i, e) => {
  8032. var className = $(e).attr('class');
  8033. if (!exContentsPageClasses.includes(className)) {
  8034. var id = "_problem_" + OJB_getRandomNumber(8);
  8035. let panel = addButtonPanel(e, id, "this_level");
  8036. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "this_level"));
  8037. promises.push(addButtonWithCopy(panel.copyButton, e, id, "this_level"));
  8038. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  8039. if (i == 0) panel.translateButton.setNotAutoTranslate(); // 题目标题块跳过,不自动翻译
  8040. }
  8041. });
  8042. });
  8043. }
  8044.  
  8045. // 添加按钮到spoiler部分
  8046. $('.spoiler-content').each((i, e) => {
  8047. if ($(e).find('.html2md-panel').length === 0) {
  8048. const id = "_spoiler_" + OJB_getRandomNumber(8);
  8049. const panel = addButtonPanel(e, id, "child_level");
  8050. promises.push(addButtonWithHTML2MD(panel.viewButton, e, id, "child_level"));
  8051. promises.push(addButtonWithCopy(panel.copyButton, e, id, "child_level"));
  8052. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  8053. }
  8054. });
  8055.  
  8056. // 添加按钮到比赛QA部分
  8057. $(".question-response").each((i, e) => {
  8058. let id = "_question_" + OJB_getRandomNumber(8);
  8059. let panel = addButtonPanel(e, id, "this_level", true);
  8060. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  8061. });
  8062. if (OJBetter.typeOfPage.is_mSite) {
  8063. $("div._ProblemsPage_announcements table tbody tr:gt(0)").each((i, e) => {
  8064. var $nextDiv = $(e).find("td:first");
  8065. let id = "_question_" + OJB_getRandomNumber(8);
  8066. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  8067. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  8068. });
  8069. }
  8070.  
  8071. // 添加按钮到弹窗confirm-proto部分
  8072. $(".confirm-proto").each((i, e) => {
  8073. let id = "_titled_" + OJB_getRandomNumber(8);
  8074. var $nextDiv = $(e).children().get(0);
  8075. let panel = addButtonPanel($nextDiv, id, "this_level", true);
  8076. promises.push(addButtonWithTranslation(panel.translateButton, $nextDiv, id, "this_level"));
  8077. });
  8078.  
  8079. // 添加按钮到_CatalogHistorySidebarFrame_item部分
  8080. $("._CatalogHistorySidebarFrame_item").each((i, e) => {
  8081. let id = "_history_sidebar_" + OJB_getRandomNumber(8);
  8082. let panel = addButtonPanel(e, id, "this_level", true);
  8083. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  8084. });
  8085.  
  8086. $(".problem-lock-link").on("click", function () {
  8087. $(".popup .content div").each((i, e) => {
  8088. let id = "_popup_" + OJB_getRandomNumber(8);
  8089. let panel = addButtonPanel(e, id, "this_level", true);
  8090. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "this_level"));
  8091. });
  8092. });
  8093.  
  8094. // 添加按钮到弹窗alert部分
  8095. $(".alert:not(.OJBetter_alert)").each((i, e) => {
  8096. let id = "_alert_" + OJB_getRandomNumber(8);
  8097. let panel = addButtonPanel(e, id, "child_level", true);
  8098. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  8099. });
  8100.  
  8101. // 添加按钮到talk-text部分
  8102. $(".talk-text").each((i, e) => {
  8103. let id = "_talk-text_" + OJB_getRandomNumber(8);
  8104. let panel = addButtonPanel(e, id, "child_level", true);
  8105. promises.push(addButtonWithTranslation(panel.translateButton, e, id, "child_level"));
  8106. });
  8107.  
  8108. return Promise.all(promises).catch(error => {
  8109. console.error("One or more of the Add Button operations failed: ", error);
  8110. });
  8111. };
  8112.  
  8113. /**
  8114. * 等待LaTeX渲染队列全部完成
  8115. * @returns {Promise} 完成渲染
  8116. */
  8117. function waitForMathJaxIdle() {
  8118. return new Promise((resolve, reject) => {
  8119. // 检查MathJax对象是否存在
  8120. const checkMathJaxExists = () => {
  8121. if (typeof MathJax === 'undefined') {
  8122. // 如果MathJax不存在,稍后再次检查
  8123. OJB_delay(100).then(checkMathJaxExists);
  8124. } else {
  8125. // MathJax存在,开始监视渲染队列
  8126. startMonitoringQueue();
  8127. }
  8128. };
  8129.  
  8130. // 开始监视MathJax渲染队列
  8131. const startMonitoringQueue = () => {
  8132. const intervalId = setInterval(() => {
  8133. const queue = MathJax.Hub.queue;
  8134. if (queue.pending === 0 && queue.running === 0) {
  8135. clearInterval(intervalId);
  8136. resolve();
  8137. }
  8138. }, 100);
  8139. };
  8140.  
  8141. // 开始检查MathJax对象
  8142. checkMathJaxExists();
  8143. });
  8144. }
  8145.  
  8146. /**
  8147. * 翻译结果面板
  8148. */
  8149. class TranslateDiv {
  8150. /**
  8151. * 构造函数
  8152. * @param {string} id 指定翻译框的id
  8153. */
  8154. constructor(id) {
  8155. this.id = id;
  8156. this.div = $('<div>').attr('id', id).addClass('translateDiv bounce-in');
  8157. if (!OJBetter.typeOfPage.is_completeProblemset) {
  8158. this.div.addClass('input-output-copier');
  8159. }
  8160. this.panelDiv = $('<div>').addClass('translate-problem-statement-panel');
  8161. this.div.append(this.panelDiv);
  8162.  
  8163. // 主要信息
  8164. this.mainDiv = $('<div>').addClass('translate-problem-statement');
  8165. this.span = $('<span>');
  8166. this.mainDiv.append(this.span);
  8167. this.div.append(this.mainDiv);
  8168. this.mainDivState = {
  8169. current: 'transHTML',
  8170. transHTML: '',
  8171. rawDataHTML: ''
  8172. };
  8173.  
  8174. // 顶栏信息
  8175. this.topText = $('<div>').addClass('topText');
  8176. this.panelDiv.append(this.topText);
  8177.  
  8178. // 右侧
  8179. this.rightDiv = $('<div>').css('display', 'flex');
  8180. this.panelDiv.append(this.rightDiv);
  8181. this.debugButton = OJB_safeCreateJQElement(`
  8182. <button class='ojb_btn ojb_btn_popover top'>
  8183. <i class="iconfont">&#xe641;</i>
  8184. <span class="popover_content">${i18next.t('rawData.normal', { ns: 'button' })}</span>
  8185. </button>`).hide();
  8186. this.rightDiv.append(this.debugButton);
  8187. this.queryBalanceButton = OJB_safeCreateJQElement(`
  8188. <button class='ojb_btn ojb_btn_popover top'>
  8189. <i class="iconfont">&#xe6ae;</i>
  8190. <span class="popover_content">${i18next.t('queryBalance.normal', { ns: 'button' })}</span>
  8191. </button>`).hide();
  8192. this.rightDiv.append(this.queryBalanceButton);
  8193. this.copyButton = OJB_safeCreateJQElement(`
  8194. <button class='ojb_btn ojb_btn_popover top'>
  8195. <i class="iconfont">&#xe608;</i>
  8196. <span class="popover_content">${i18next.t('copy.normal', { ns: 'button' })}</span>
  8197. </button>`);
  8198. this.rightDiv.append(this.copyButton);
  8199. this.upButton = OJB_safeCreateJQElement(`
  8200. <button class='ojb_btn ojb_btn_popover top'>
  8201. <i class="iconfont">&#xe601;</i>
  8202. <span class="popover_content">${i18next.t('fold.normal', { ns: 'button' })}</span>
  8203. </button>`);
  8204. this.rightDiv.append(this.upButton);
  8205. this.closeButton = OJB_safeCreateJQElement(`
  8206. <button class='ojb_btn ojb_btn_popover top'>
  8207. <i class="iconfont">&#xe614;</i>
  8208. <span class="popover_content">${i18next.t('close.normal', { ns: 'button' })}</span>
  8209. </button>`);
  8210. this.rightDiv.append(this.closeButton);
  8211. }
  8212.  
  8213. /**
  8214. * 获取翻译框
  8215. * @returns {JQuery<HTMLElement>} 返回翻译框
  8216. */
  8217. getDiv() {
  8218. return this.div;
  8219. }
  8220.  
  8221. /**
  8222. * 设置翻译框顶部的文本
  8223. * @param {string} text 翻译框顶部的文本
  8224. */
  8225. setTopText(text) {
  8226. this.div.attr("data-topText", text);
  8227. this.topText.text(text);
  8228. }
  8229.  
  8230. /**
  8231. * 获取翻译框顶部的文本
  8232. * @returns {string} 返回翻译框顶部的文本
  8233. */
  8234. getTopText() {
  8235. return this.topText.text();
  8236. }
  8237.  
  8238. /**
  8239. * 渲染一个元素内的LaTeX公式
  8240. * @param {HTMLElement} element 元素
  8241. */
  8242. renderLaTeX(element) {
  8243. MathJax.Hub.Queue(["Typeset", MathJax.Hub, element]);
  8244. }
  8245.  
  8246. /**
  8247. * 更新翻译框内容
  8248. * @param {string} text 文本内容
  8249. * @param {boolean} is_escapeHTML 是否转义HTML标签,为true则HTML标签将作为普通文本处理,默认为true
  8250. * @param {boolean} is_renderLaTeX 是否渲染LaTeX,为true则会渲染LaTeX,默认为true
  8251. */
  8252. updateTranslateDiv(text, is_escapeHTML = true, is_renderLaTeX = true,) {
  8253. // 渲染MarkDown
  8254. let md = window.markdownit({
  8255. html: !is_escapeHTML,
  8256. });
  8257. if (!text) text = "";
  8258. let html = md.render(text);
  8259. this.mainDiv.html(html);
  8260. // 渲染Latex
  8261. if (is_renderLaTeX) {
  8262. // MathJax.Hub.Queue(["Typeset", MathJax.Hub, this.mainDiv.get(0)]);
  8263. this.renderLaTeX(this.mainDiv.get(0));
  8264. }
  8265. // // 渲染代码块中的公式 (AtCoder)
  8266. // this.mainDiv.find('pre code').each((index, element) => {
  8267. // const codeText = $(element).text();
  8268. // const latexPattern = /\$\$(\\.|[^\$])*?\$\$|\$(\\.|[^\$])*?\$/;
  8269. // if (latexPattern.test(codeText)) {
  8270. // this.renderLaTeX(element);
  8271. // }
  8272. // });
  8273. }
  8274.  
  8275. /**
  8276. * 关闭元素
  8277. */
  8278. close() {
  8279. this.closeButton.click();
  8280. }
  8281.  
  8282. /**
  8283. * 收起元素
  8284. */
  8285. fold() {
  8286. if (!this.upButton.hasClass("reverse")) {
  8287. this.upButton.click();
  8288. }
  8289. }
  8290.  
  8291. /**
  8292. * 注册收起按钮事件
  8293. */
  8294. registerUpButtonEvent() {
  8295. this.upButton.on("click", () => {
  8296. // 如果没有reverse类,说明是展开状态
  8297. if (!this.upButton.hasClass("reverse")) {
  8298. // 执行收起操作
  8299. this.upButton.addClass("reverse");
  8300. this.upButton.setButtonState('initial', i18next.t('fold.unfold', { ns: 'button' }));
  8301. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8302. } else {
  8303. // 执行展开操作
  8304. this.upButton.removeClass("reverse");
  8305. this.upButton.setButtonState('initial', i18next.t('fold.normal', { ns: 'button' }));
  8306. OJB_toggleCollapseExpand(this.mainDiv.get(0));
  8307. }
  8308. });
  8309. }
  8310.  
  8311. /**
  8312. * 注册关闭按钮事件
  8313. */
  8314. registerCloseButtonEvent() {
  8315. this.closeButton.on("click", () => {
  8316. $(this.div).remove();
  8317. $(this.panelDiv).remove();
  8318. if (OJBetter.typeOfPage.is_problem && OJBetter.translation.memory.enabled) {
  8319. OJBetter.translation.memory.ttTree.rmTransResultMap(this.id); // 移除ttTree中的数据
  8320. OJBetter.translation.memory.ttTree.refreshNode(".ttypography");
  8321. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新DB中的数据
  8322. }
  8323. });
  8324. }
  8325.  
  8326. /**
  8327. * 注册复制按钮事件
  8328. * @param {string} text 复制的文本
  8329. */
  8330. registerCopyButtonEvent(text) {
  8331. this.copyButton.on("click", () => {
  8332. GM_setClipboard(text);
  8333. this.copyButton.setButtonState('success', i18next.t('copy.copied', { ns: 'button' }));
  8334. // 复制提示
  8335. setTimeout(() => {
  8336. this.copyButton.setButtonState('initial', i18next.t('copy.normal', { ns: 'button' }));
  8337. }, 2000);
  8338. });
  8339. }
  8340.  
  8341. /**
  8342. * 禁用复制按钮
  8343. */
  8344. disableCopyButton() {
  8345. this.copyButton.css({ 'fill': '#ccc' });
  8346. this.copyButton.off("click");
  8347. }
  8348.  
  8349. /**
  8350. * 设置面板为error状态
  8351. */
  8352. setError() {
  8353. this.div.addClass('error');
  8354. this.panelDiv.addClass('error');
  8355. this.mainDiv.addClass('error');
  8356. }
  8357.  
  8358. /**
  8359. * 设置原始数据数据
  8360. * @param {Object} Object 原始数据
  8361. */
  8362. setRawData(Object) {
  8363. this.mainDivState.rawDataHTML = $("<pre>").text(JSON.stringify(Object, null, 4)).get(0);
  8364. if (this.mainDivState.current === 'rawDataHTML') {
  8365. this.renderMainDiv();
  8366. }
  8367. }
  8368.  
  8369. /**
  8370. * 切换结果面板与原始数据面板
  8371. */
  8372. switchMainDiv() {
  8373. // 在切换之前,保存当前内容的状态
  8374. this.mainDivState[this.mainDivState.current] = this.mainDiv.html();
  8375. // 切换当前状态
  8376. this.debugButton.setButtonState(this.mainDivState.current === 'transHTML' ? 'enabled' : 'initial');
  8377. this.mainDivState.current = this.mainDivState.current === 'transHTML' ? 'rawDataHTML' : 'transHTML';
  8378. // 渲染新的当前状态
  8379. this.renderMainDiv();
  8380. }
  8381.  
  8382. // 渲染当前内容到 mainDiv
  8383. renderMainDiv() {
  8384. requestAnimationFrame(() => {
  8385. this.mainDiv.html(this.mainDivState[this.mainDivState.current]);
  8386. });
  8387. }
  8388.  
  8389. /**
  8390. * 注册debug按钮事件
  8391. */
  8392. registerDebugButtonEvent() {
  8393. this.debugButton.on("click", () => {
  8394. this.switchMainDiv();
  8395. });
  8396. }
  8397.  
  8398. /**
  8399. * 显示debug按钮
  8400. */
  8401. showDebugButton() {
  8402. this.debugButton.show();
  8403. this.registerDebugButtonEvent();
  8404. }
  8405.  
  8406. /**
  8407. * 注册查询余额按钮事件
  8408. * @param {function} callback 查询回调函数
  8409. */
  8410. registerQueryBalanceButtonEvent(callback) {
  8411. this.queryBalanceButton.on("click", async () => {
  8412. this.queryBalanceButton.setButtonState('loading', i18next.t('queryBalance.loading', { ns: 'button' }));
  8413. try {
  8414. const balance = await callback();
  8415. this.queryBalanceButton.setButtonState('success', `${i18next.t('queryBalance.success', { ns: 'button' })} ${balance}`);
  8416. } catch (error) {
  8417. this.queryBalanceButton.setButtonState('error', `${i18next.t('queryBalance.error', { ns: 'button' })} ${error.message}`);
  8418. }
  8419. });
  8420. }
  8421.  
  8422. /**
  8423. * 显示余额查询按钮
  8424. * @param {string} server 服务名称
  8425. */
  8426. showQueryBalanceButton(server) {
  8427. if (server == 'deepl') {
  8428. const quotaConfig = OJBetter.deepl.config.quota;
  8429. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8430. this.queryBalanceButton.show();
  8431. this.registerQueryBalanceButtonEvent(() => {
  8432. return queryServerBalance(OJBetter.deepl.config.quota);
  8433. });
  8434. }
  8435. } else if (server == 'openai') {
  8436. const quotaConfig = OJBetter.chatgpt.config.quota;
  8437. if (quotaConfig.url && quotaConfig.surplus && quotaConfig.header) {
  8438. this.queryBalanceButton.show();
  8439. this.registerQueryBalanceButtonEvent(() => {
  8440. return queryServerBalance(OJBetter.chatgpt.config.quota);
  8441. });
  8442. }
  8443. }
  8444. }
  8445. }
  8446.  
  8447. // 元素关系树
  8448. class ElementsTree {
  8449. constructor(elements) {
  8450. this.node = [];
  8451. this.transResultMap = {};
  8452. this.index = 0;
  8453. this.tagNames = ["DIV", "P", "UL", "LI"]
  8454. this.init($(elements));
  8455. }
  8456.  
  8457. // Iterate through all elements, because there may be multiple ttypography
  8458. init(elements) {
  8459. elements.each((i, e) => {
  8460. this.node.push({}); // add one element
  8461. this.index = 0; // reset index
  8462. this.create(i, $(e));
  8463. });
  8464. }
  8465.  
  8466. // 刷新关系树
  8467. refreshNode(elements) {
  8468. this.node = [];
  8469. this.index = 0;
  8470. this.init($(elements));
  8471. }
  8472.  
  8473. // 创建节点间的关系树
  8474. create(i_, element) {
  8475. var prev = null;
  8476. var node = this.node[i_];
  8477. element.children().each((i, e) => {
  8478. // only add element with tagNames
  8479. if (this.tagNames.includes($(e).prop("tagName"))) {
  8480. prev = this.addNode(i_, prev, e);
  8481. }
  8482. // recursively child element
  8483. if ($(e).children().length > 0 && prev !== null) {
  8484. node[prev].firstChild = this.index;
  8485. this.create(i_, $(e));
  8486. }
  8487. });
  8488. }
  8489.  
  8490. // 向树中添加一个节点
  8491. addNode(i_, prev, e) {
  8492. let node = this.node[i_];
  8493. node[this.index] = {
  8494. prev: prev,
  8495. next: null,
  8496. firstChild: null,
  8497. type: $(e).prop("tagName"),
  8498. isTranslateDiv: $(e).hasClass("translateDiv"),
  8499. topText: $(e).attr("data-topText"),
  8500. id: $(e).attr("id"),
  8501. };
  8502.  
  8503. if (prev !== null) {
  8504. node[prev].next = this.index;
  8505. }
  8506.  
  8507. prev = this.index;
  8508.  
  8509. this.index++;
  8510. return prev;
  8511. }
  8512.  
  8513. getNodeData() {
  8514. return this.node;
  8515. }
  8516.  
  8517. setNodeData(node) {
  8518. this.node = node;
  8519. }
  8520.  
  8521. getTransResultMap() {
  8522. return this.transResultMap;
  8523. }
  8524.  
  8525. setTransResultMap(transResultMap) {
  8526. this.transResultMap = transResultMap;
  8527. }
  8528.  
  8529. rmTransResultMap(id) {
  8530. delete this.transResultMap[id];
  8531. }
  8532.  
  8533. addTransResultMap(id, text) {
  8534. this.transResultMap[id] = text;
  8535. }
  8536.  
  8537. getTranslateDivNum(ttTree) {
  8538. var num = 0;
  8539. for (var i in ttTree) {
  8540. if (ttTree[i].isTranslateDiv) {
  8541. num++;
  8542. }
  8543. }
  8544. return num;
  8545. }
  8546.  
  8547. // 恢复目标元素中的translateDiv
  8548. recover(elements) {
  8549. elements.each((i, e) => {
  8550. var ttTreeNode = this.node[i];
  8551. var missingTranslateDivs = this.getTranslateDivNum(ttTreeNode);
  8552. if (missingTranslateDivs > 0) {
  8553. this.recoverOneElement($(e), ttTreeNode);
  8554. }
  8555. });
  8556. }
  8557.  
  8558. recoverOneElement(element, ttTreeNode) {
  8559. this.recoverOneFork(element.children().eq(0), ttTreeNode, 0);
  8560. }
  8561.  
  8562. // 恢复一个分支
  8563. recoverOneFork(pElement, ttTreeNode, index) {
  8564. do {
  8565. // only recover element with tagNames
  8566. if (!this.tagNames.includes(pElement.prop("tagName"))) {
  8567. if (pElement.next().length > 0) {
  8568. pElement = pElement.next();
  8569. } else {
  8570. return;
  8571. }
  8572. }
  8573. if (!ttTreeNode[index] || pElement.prop("tagName") !== ttTreeNode[index].type) {
  8574. // console.warn(`元素不存在或类型不同, 元素结构可能已经发生了变化: \nindex: ${index}`, pElement);
  8575. return;
  8576. } else {
  8577. // recursively child element
  8578. var node = ttTreeNode[index];
  8579. if (node.firstChild !== null) {
  8580. this.recoverOneFork(
  8581. pElement.children().eq(0),
  8582. ttTreeNode,
  8583. node.firstChild
  8584. );
  8585. }
  8586. // check if next node is translateDiv
  8587. if (node.next !== null) {
  8588. index = node.next;
  8589.  
  8590. var ne_node = ttTreeNode[index];
  8591. if (ne_node.isTranslateDiv) {
  8592. var id = ne_node.id;
  8593. var topText = ne_node.topText;
  8594. var text = this.transResultMap[id];
  8595. // create element after pElement
  8596. this.reCreateTransDiv(pElement, id, text, topText, node.isTranslateDiv); // 如果前面一个也是翻译结果,则该结果折叠
  8597. }
  8598. pElement = pElement.next(); // go to next element
  8599. }
  8600. }
  8601. } while (node.next !== null);
  8602. }
  8603.  
  8604. /**
  8605. * 重新创建translateDiv
  8606. * @param {*} pElement
  8607. * @param {*} id
  8608. * @param {*} translatedText
  8609. * @param {*} topText
  8610. * @param {Boolean} isFold 是否折叠
  8611. */
  8612. reCreateTransDiv(pElement, id, translatedText, topText, isFold) {
  8613. const translateDiv = new TranslateDiv(id);
  8614. pElement.after(translateDiv.getDiv());
  8615. translateDiv.setTopText(topText);
  8616. translateDiv.registerUpButtonEvent();
  8617. translateDiv.registerCloseButtonEvent();
  8618. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8619. translateDiv.registerCopyButtonEvent(translatedText);
  8620. } else {
  8621. translateDiv.disableCopyButton();
  8622. }
  8623. translateDiv.updateTranslateDiv(translatedText, !((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion)));
  8624. // 标记已翻译并添加到翻译按钮的结果栈中
  8625. let transButton = pElement.prev('.html2md-panel').find('.translateButton');
  8626. if (transButton.length == 0) {
  8627. // 如果没有找到,则应该是得在父元素中找到
  8628. transButton = pElement.parent().prev('.html2md-panel').find('.translateButton');
  8629. }
  8630. if (isFold) translateDiv.fold(); // 是否折叠该翻译
  8631. transButton.pushResultToTransButton({
  8632. translateDiv: translateDiv,
  8633. status: 0
  8634. });
  8635. transButton.setTransButtonState('success');
  8636. }
  8637. }
  8638.  
  8639. // 更新TransDB中的翻译数据
  8640. async function updateTransDBData(nodeDate, transResultMap) {
  8641. var url = window.location.href.replace(/#/, "");
  8642. try {
  8643. await OJBetter.common.database.translateData.put({ url, transResultMap, nodeDate });
  8644. return 'translateData saved successfully';
  8645. } catch (error) {
  8646. throw new Error(`Failed to save translateData: ${error}`);
  8647. }
  8648. }
  8649.  
  8650. // 获取TransDB中保存的翻译数据
  8651. async function getTransDBData() {
  8652. var url = window.location.href.replace(/#/, "");
  8653. try {
  8654. const result = await OJBetter.common.database.translateData.get(url);
  8655. return result;
  8656. } catch (error) {
  8657. throw new Error(`Failed to get translateData: ${error}`);
  8658. }
  8659. }
  8660.  
  8661. /**
  8662. * 翻译结果恢复功能初始化
  8663. * @returns
  8664. */
  8665. async function initTransResultsRecover() {
  8666. OJBetter.translation.memory.ttTree = new ElementsTree(".ttypography"); // 初始化当前页面.ttypography元素的结构树
  8667. let result = await getTransDBData();
  8668. if (!result) return;
  8669. OJBetter.translation.memory.ttTree.setNodeData(result.nodeDate);
  8670. OJBetter.translation.memory.ttTree.setTransResultMap(result.transResultMap);
  8671. OJBetter.translation.memory.ttTree.recover($(".ttypography"));
  8672. }
  8673.  
  8674. /**
  8675. * 自动翻译
  8676. */
  8677. async function initTransWhenViewable() {
  8678. await waitForMathJaxIdle();
  8679.  
  8680. const elements = $('.ttypography, .comments').find('.translateButton');
  8681. const observers = [];
  8682.  
  8683. // Use a single Intersection Observer for all elements
  8684. const observer = new IntersectionObserver((entries, obs) => {
  8685. entries.forEach((entry) => {
  8686. if (entry.isIntersecting) {
  8687. const button = $(entry.target);
  8688. const state = button.getButtonState();
  8689. const notAutoTranslate = button.getNotAutoTranslate();
  8690. // Check if the button meets the criteria
  8691. if (state === 'normal' && !notAutoTranslate) {
  8692. let trans = OJBetter.translation.choice;
  8693.  
  8694. if (OJBetter.translation.auto.mixTrans.enabled && button.IsCommentButton() && OJBetter.translation.auto.mixTrans.servers.length > 0) {
  8695. const randomIndex = Math.floor(Math.random() * OJBetter.translation.auto.mixTrans.servers.length);
  8696. trans = OJBetter.translation.auto.mixTrans.servers[randomIndex];
  8697. }
  8698. button.data("translatedItBy")(trans);
  8699. }
  8700.  
  8701. // Stop observing the element
  8702. obs.unobserve(entry.target);
  8703. }
  8704. });
  8705. });
  8706.  
  8707. // Observe each element
  8708. elements.each((i, e) => {
  8709. observer.observe(e);
  8710. });
  8711.  
  8712. // Store the observer in case you need to disconnect it later
  8713. observers.push(observer);
  8714. }
  8715.  
  8716. /**
  8717. * 翻译返回结果结构体
  8718. * @typedef {Object} TranslateResult
  8719. * @property {string} status 翻译状态
  8720. * @property {TranslateDiv} translateDiv 翻译结果面板
  8721. * @property {TransRawData} rawData 原始翻译数据
  8722. */
  8723.  
  8724. /**
  8725. * 翻译主方法
  8726. * @param {string} text 待翻译文本
  8727. * @param {HTMLElement} element_node 元素节点
  8728. * @param {Boolean} is_comment 是否为评论区文本
  8729. * @param {string} overrideTrans 覆盖全局翻译服务设定
  8730. * @returns {TranslateResult} 翻译结果对象
  8731. */
  8732. async function translateProblemStatement(text, element_node, is_comment, overrideTrans) {
  8733. /** @type {number} 翻译结果的ID*/
  8734. const id = OJB_getRandomNumber(8);
  8735. /** @type {TextBlockReplacer} 文本块替换/恢复实例*/
  8736. const textBlockReplacer = new TextBlockReplacer();
  8737. /** @type {string} 翻译结果文本*/
  8738. let translatedText = "";
  8739.  
  8740. /** @type {string} 当前实际应用的翻译服务 */
  8741. const realTransServer = overrideTrans ||
  8742. (is_comment && OJBetter.translation.comment.choice != "0" ?
  8743. OJBetter.translation.comment.choice :
  8744. OJBetter.translation.choice);
  8745.  
  8746. /** @type {TranslateResult} 翻译结果对象 */
  8747. const translateResult = {
  8748. status: "ok",
  8749. rawData: {
  8750. done: false
  8751. }
  8752. }
  8753.  
  8754. /**
  8755. * LaTeX替换
  8756. * @param {string} text 待翻译文本
  8757. * @returns {string} 处理后的文本
  8758. */
  8759. const replaceLatex = function (text) {
  8760. if (OJBetter.typeOfPage.is_oldLatex) {
  8761. const regex = /<span\s+class="tex-span">.*?<\/span>/gi;
  8762. text = textBlockReplacer.replace(text, regex);
  8763. text = text.replace(/<p>(.*?)<\/p>/g, "$1\n\n"); // <p/>标签换为换行
  8764. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8765. const regex = /<i>.*?<\/i>|<sub>.*?<\/sub>|<sup>.*?<\/sup>|<pre>.*?<\/pre>/gi; // TODO 111
  8766. text = textBlockReplacer.replace(text, regex);
  8767. } else if (realTransServer != "openai") {
  8768. // 使用GPT翻译时不必替换latex公式
  8769. const regex = /\$\$([^]*?)\$\$|\$(\\\$|[^\$])*?\$/g;
  8770. text = textBlockReplacer.replace(text, regex);
  8771.  
  8772. // 替换行间代码块```
  8773. const regex2 = /```[\s\S]*?```/g;
  8774. text = textBlockReplacer.replace(text, regex2);
  8775. }
  8776. return text;
  8777. }
  8778.  
  8779. /**
  8780. * LaTeX恢复
  8781. * @param {string} text 已翻译的文本
  8782. * @returns {string} 恢复后的文本
  8783. */
  8784. const recoverLatex = function (text) {
  8785. // 两个公式之间加个空格,防止有些LaTeX解析器解析错误
  8786. let resultText = text
  8787. .replace(/】【/g, '】 【')
  8788. .replace(/\]\[/g, '] [')
  8789. .replace(/\}\{/g, '} {');
  8790.  
  8791. if (OJBetter.typeOfPage.is_oldLatex) {
  8792. resultText = resultText.replace(/(.+?)(\n\n|$)/g, "<p>$1</p>"); // 换行符还原为<p/>标签
  8793. resultText = textBlockReplacer.recover(resultText);
  8794. } else if (OJBetter.typeOfPage.is_acmsguru) {
  8795. resultText = textBlockReplacer.recover(resultText);
  8796. } else if (realTransServer != "openai") {
  8797. resultText = textBlockReplacer.recover(resultText);
  8798. }
  8799. return resultText;
  8800. }
  8801.  
  8802. /**
  8803. * 格式化翻译结果
  8804. * @param {string} text
  8805. * @returns {string} 处理后的翻译结果
  8806. */
  8807. const formatText = function (text) {
  8808. // 转义LaTex中的特殊符号
  8809. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  8810. // 先替换掉行间代码块
  8811. const replacer = new TextBlockReplacer();
  8812. text = replacer.replace(text, /```[\s\S]*?```/g);
  8813.  
  8814. // 处理LaTeX公式
  8815. const escapeRules = [
  8816. { pattern: /(?<!\\)>(?!\s)/g, replacement: " &gt; " }, // >符号
  8817. { pattern: /(?<!\\)</g, replacement: " &lt; " }, // <符号
  8818. { pattern: /(?<!\\)\*/g, replacement: " &#42; " }, // *符号
  8819. { pattern: /(?<!\\)_/g, replacement: " &#95; " }, // _符号
  8820. { pattern: /(?<!\\)\\\\(?=\s)/g, replacement: "\\\\\\\\" }, // \\符号
  8821. { pattern: /(?<!\\)\\(?![\\a-zA-Z0-9])/g, replacement: "\\\\" }, // \符号
  8822. ];
  8823.  
  8824. let latexMatches = [...text.matchAll(/\$\$([\s\S]*?)\$\$|\$(.*?)\$|\$([\s\S]*?)\$/g)];
  8825. for (const match of latexMatches) {
  8826. const matchedText = match[0];
  8827. let escapedText = matchedText;
  8828.  
  8829. for (const rule of escapeRules) {
  8830. escapedText = escapedText.replaceAll(rule.pattern, rule.replacement);
  8831. }
  8832. escapedText = escapedText.replace(/\$\$/g, "$$$$$$$$");// $$符号(因为后面需要作为replacement,双倍消耗)
  8833. text = text.replace(matchedText, escapedText);
  8834. }
  8835.  
  8836. // 恢复行间代码块
  8837. text = replacer.recover(text);
  8838. }
  8839.  
  8840. // 使符合mathjx的转换语法
  8841. const mathjaxRuleMap = [
  8842. { pattern: /\$/g, replacement: "$$$$$$" }, // $$ 行间
  8843. ];
  8844. mathjaxRuleMap.forEach(({ pattern, replacement }) => {
  8845. text = text.replace(pattern, replacement);
  8846. });
  8847.  
  8848. // markdown修正
  8849. const mdRuleMap = [
  8850. { pattern: /(\s_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: "$1 $2" }, // 斜体
  8851. { pattern: /(_[\u4e00-\u9fa5]+_\s)([\u4e00-\u9fa5]+)/g, replacement: " $1$2" },
  8852. { pattern: /(_[\u4e00-\u9fa5]+_)([\u4e00-\u9fa5]+)/g, replacement: " $1 $2" },
  8853. { pattern: /(([\s\S]*?))/g, replacement: "($1)" }, // 中文()
  8854. // { pattern: /:/g, replacement: ":" }, // 中文:
  8855. { pattern: /\*\* (.*?) \*\*/g, replacement: "\*\*$1\*\*" } // 加粗
  8856. ];
  8857. mdRuleMap.forEach(({ pattern, replacement }) => {
  8858. text = text.replace(pattern, replacement);
  8859. });
  8860.  
  8861. return text;
  8862. }
  8863.  
  8864. // 创建翻译结果元素并放在element_node的后面
  8865. translateResult.translateDiv = new TranslateDiv(id);
  8866. $(element_node).after(translateResult.translateDiv.getDiv());
  8867.  
  8868. // 顶栏左侧信息
  8869. translateResult.translateDiv.setTopText(i18next.t('servers.' + realTransServer, { ns: 'translator' }) +
  8870. i18next.t('translateDiv.topTextSuffix', { ns: 'translator' }));
  8871.  
  8872. // 注册按钮
  8873. translateResult.translateDiv.registerUpButtonEvent();
  8874. translateResult.translateDiv.registerCloseButtonEvent();
  8875. if (OJBetter.translation.choice == 'openai' || OJBetter.translation.choice == 'deepl') {
  8876. translateResult.translateDiv.showQueryBalanceButton(OJBetter.translation.choice); // 显示额度查询
  8877. }
  8878.  
  8879. // 翻译内容是否为空文本
  8880. if (isEmptyText(text)) {
  8881. const shouldContinue = await OJB_createDialog(
  8882. i18next.t('isEmptyText.title', { ns: 'dialog' }),
  8883. i18next.t('isEmptyText.content', { ns: 'dialog' }),
  8884. [
  8885. i18next.t('isEmptyText.buttons.0', { ns: 'dialog' }),
  8886. i18next.t('isEmptyText.buttons.1', { ns: 'dialog' })
  8887. ],
  8888. true
  8889. );
  8890. if (shouldContinue) {
  8891. translateResult.status = "skip";
  8892. return translateResult;
  8893. }
  8894. }
  8895.  
  8896. // 替换latex公式
  8897. text = replaceLatex(text);
  8898.  
  8899. // 过滤**号
  8900. if (OJBetter.translation.filterTextWithoutEmphasis && GM_getValue("translation") !== "openai") { // TODO
  8901. text = text.replace(/\*\*/g, "");
  8902. }
  8903.  
  8904. // 字符数上限
  8905. const translationLimits = {
  8906. deepl: 5000,
  8907. iflyrec: 2000,
  8908. youdao: 5000,
  8909. google: 5000,
  8910. caiyun: 5000
  8911. };
  8912. if (translationLimits.hasOwnProperty(realTransServer) && text.length > translationLimits[realTransServer]) {
  8913. let textLength = translationLimits[realTransServer];
  8914. let realTextLength = text.length;
  8915. const shouldContinue = await OJB_createDialog(
  8916. i18next.t('transTextLimits.title', { ns: 'dialog' }),
  8917. i18next.t('transTextLimits.content', { ns: 'dialog', textLength: textLength, realTextLength: realTextLength }),
  8918. [
  8919. i18next.t('transTextLimits.buttons.0', { ns: 'dialog' }),
  8920. i18next.t('transTextLimits.buttons.1', { ns: 'dialog' })
  8921. ],
  8922. true
  8923. ); // 字数超限确认
  8924. if (shouldContinue) {
  8925. translateResult.status = "skip";
  8926. return translateResult;
  8927. }
  8928. }
  8929.  
  8930. /**
  8931. * 调用各个翻译服务
  8932. * @param {string} transServer 翻译服务
  8933. * @returns {TransRawData} 原始翻译数据
  8934. */
  8935. async function translate(transServer) {
  8936. const is_renderLaTeX = !((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion));
  8937. const servername = i18next.t('servers.' + realTransServer, { ns: 'translator' });
  8938. /** @type {TransRawData} 原始翻译数据*/
  8939. let rawData = {};
  8940. try {
  8941. if (transServer == "deepl") {
  8942. if (OJBetter.deepl.config.type == 'free') {
  8943. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8944. rawData = await translate_deepl(text);
  8945. } else if (OJBetter.deepl.config.type == 'api') {
  8946. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.deeplApi', { ns: 'translator', deepl_configName: OJBetter.deepl.config.name })}`, is_renderLaTeX);
  8947. if (OJBetter.deepl.config.apiGenre == 'deeplx') {
  8948. rawData = await translate_deeplx(text);
  8949. } else {
  8950. if (OJBetter.deepl.enableEmphasisProtection) text = convertBoldMarkdownToHTML(text);
  8951. if (OJBetter.deepl.enableLinkProtection) text = convertLinksMarkdownToHTML(text);
  8952. if (OJBetter.deepl.config.apiGenre == 'api-free') {
  8953. rawData = await translate_deepl_api_free(text);
  8954. } else if (OJBetter.deepl.config.apiGenre == 'api-pro') {
  8955. rawData = await translate_deepl_api_pro(text);
  8956. }
  8957. if (OJBetter.deepl.enableEmphasisProtection) rawData.text = convertBoldHTMLToMarkdown(rawData.text);
  8958. if (OJBetter.deepl.enableLinkProtection) rawData.text = convertLinksHTMLToMarkdown(rawData.text);
  8959. }
  8960. }
  8961. } else if (transServer == "iflyrec") {
  8962. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8963. rawData = await translate_iflyrec(text);
  8964. } else if (transServer == "youdao") {
  8965. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8966. rawData = await translate_youdao_mobile(text);
  8967. } else if (transServer == "google") {
  8968. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8969. rawData = await translate_gg(text);
  8970. } else if (transServer == "caiyun") {
  8971. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.basic', { ns: 'translator', server: servername })}`, is_renderLaTeX);
  8972. rawData = await translate_caiyun(text);
  8973. } else if (transServer == "openai") {
  8974. translateResult.translateDiv.updateTranslateDiv(`${i18next.t('transingTip.openai', { ns: 'translator', openai_name: OJBetter.chatgpt.config.name })}${!OJBetter.chatgpt.isStream
  8975. ? i18next.t('transingTip.openai_isStream', { ns: 'translator' }) : ""}`,
  8976. is_renderLaTeX);
  8977. if (OJBetter.chatgpt.isStream) {
  8978. // 流式传输
  8979. rawData = await translate_openai_stream(text, translateResult.translateDiv);
  8980. } else {
  8981. // 普通模式
  8982. rawData = await translate_openai(text);
  8983. }
  8984. }
  8985. translatedText = rawData.text;
  8986. if (!rawData.done) {
  8987. translateResult.status = "error";
  8988. }
  8989. } catch (e) {
  8990. translateResult.status = "error";
  8991. rawData.message = i18next.t('error.unexpected', { ns: 'translator' });
  8992. console.warn(e);
  8993. }
  8994. return rawData;
  8995. }
  8996. translateResult.rawData = await translate(realTransServer);
  8997.  
  8998. if (translateResult.status == "error") {
  8999. translateResult.translateDiv.updateTranslateDiv(translateResult.rawData.message);
  9000. return translateResult;
  9001. }
  9002.  
  9003. // 还原latex公式
  9004. translatedText = recoverLatex(translatedText);
  9005.  
  9006. // 注册结果复制按钮
  9007. if (!OJBetter.typeOfPage.is_oldLatex && !OJBetter.typeOfPage.is_acmsguru) {
  9008. translateResult.translateDiv.registerCopyButtonEvent(translatedText);
  9009. } else {
  9010. translateResult.translateDiv.disableCopyButton();
  9011. }
  9012.  
  9013. // 翻译结果格式化
  9014. translatedText = formatText(translatedText);
  9015.  
  9016. // 保存翻译结果
  9017. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  9018. OJBetter.translation.memory.ttTree.refreshNode(".ttypography"); // 刷新当前页面.ttypography元素的结构树实例
  9019. OJBetter.translation.memory.ttTree.addTransResultMap(id, translatedText);
  9020. updateTransDBData(OJBetter.translation.memory.ttTree.getNodeData(), OJBetter.translation.memory.ttTree.getTransResultMap()); // 更新翻译结果到transDB
  9021. }
  9022.  
  9023. // 翻译结果面板更新
  9024. translateResult.translateDiv.updateTranslateDiv(translatedText, !((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion)));
  9025.  
  9026. return translateResult;
  9027. }
  9028.  
  9029. //弹窗翻译
  9030. function alertZh() {
  9031. // var _alert = window.alert;
  9032. // window.alert = async function (msg) {
  9033. // _alert(msg + "\n=========翻译=========\n" + await translate_deepl(msg));
  9034. // return true;
  9035. // }
  9036. };
  9037.  
  9038. /**
  9039. * 折叠块展开
  9040. */
  9041. function ExpandFoldingblocks() {
  9042. $('.spoiler').addClass('spoiler-open');
  9043. $('.spoiler-content').attr('style', '');
  9044. };
  9045.  
  9046. /**
  9047. * 折叠块渲染优化
  9048. */
  9049. function RenderPerfOpt() {
  9050. GM_addStyle(`
  9051. .spoiler-content {
  9052. contain: layout style;
  9053. }
  9054. `);
  9055. }
  9056.  
  9057. /**
  9058. * 下拉选择框性能优化
  9059. */
  9060. async function SelectElementPerfOpt() {
  9061. // TODO 10
  9062. // 加载库资源
  9063. await OJB_LoadJS("https://aowuucdn.oss-accelerate.aliyuncs.com/js/selectpage.min.js");
  9064. /**
  9065. * 将一个<select>元素转换为SelectPage控件
  9066. * @param {HTMLElement|string} selector - 要转换的<select>元素或其选择器
  9067. */
  9068. const OJB_transformSelectToSelectPage = (selector) => {
  9069. const $select = $(selector);
  9070. if ($select.length === 0 || !$select.is('select')) {
  9071. console.error('Invalid select element provided.');
  9072. return;
  9073. }
  9074.  
  9075. // 隐藏原生的<select>元素
  9076. $select.hide();
  9077.  
  9078. // 创建一个新的<input>元素用于SelectPage控件
  9079. const $inputForSelectPage = $('<input>', {
  9080. type: 'text',
  9081. class: 'selectpage-input',
  9082. autocomplete: 'off'
  9083. });
  9084. $select.after($inputForSelectPage);
  9085.  
  9086. // 准备SelectPage所需的数据格式
  9087. const data = $select.find('option').map((_, option) => ({
  9088. id: option.value,
  9089. text: option.text
  9090. })).get();
  9091.  
  9092. // 初始化SelectPage
  9093. $inputForSelectPage.selectPage({
  9094. showField: 'text',
  9095. keyField: 'id',
  9096. data,
  9097. lang: 'en',
  9098. // 当选中一个选项时,更新隐藏的<select>元素的值
  9099. eSelect: (data) => {
  9100. $select.val(data.id).trigger('change');
  9101. },
  9102. // 初始化时根据<select>的当前值设置SelectPage
  9103. initRecord: $select.val()
  9104. });
  9105. };
  9106.  
  9107. // 遍历页面上的所有select
  9108. $('select').each((_, select) => {
  9109. // 选项大于500才优化
  9110. if ($(select).find('option').length > 500) {
  9111. OJB_transformSelectToSelectPage(select);
  9112. }
  9113. });
  9114. }
  9115.  
  9116. /**
  9117. * 评论区分页
  9118. */
  9119. function CommentPagination() {
  9120. GM_addStyle(`
  9121. .comments > .comment {
  9122. display: none;
  9123. }
  9124. #next-page-btn, #prev-page-btn {
  9125. display: none;
  9126. }
  9127. #jump-input, #items-per-page{
  9128. height: 22px;
  9129. border: 1px solid #dcdfe6;
  9130. border-radius: 0.3rem;
  9131. }
  9132. #jump-input:focus-visible{
  9133. border-style: solid;
  9134. border-color: #3f51b5;
  9135. outline: none;
  9136. }
  9137. `);
  9138. $('.comments').after(`
  9139. <div id="pagBar" style="display: flex; align-items: center; justify-content: center; color: #606266;">
  9140. <label for="items-per-page">${i18next.t('perpage', { ns: 'comments' })}</label>
  9141. <select id="items-per-page" style="margin-right: 15px;">
  9142. <option value="5">5</option>
  9143. <option value="10">10</option>
  9144. <option value="20">20</option>
  9145. </select>
  9146. <div class="paging" style="margin-right: 15px;">
  9147. <span id="current-page">1</span> / <span id="total-pages"></span>
  9148. </div>
  9149. <input type="text" id="jump-input" placeholder="${i18next.t('jumpTo', { ns: 'comments' })}">
  9150. <button type="button" id="jump-btn" class="ojb_btn">${i18next.t('jump', { ns: 'comments' })}</button>
  9151. <button id="prev-page-btn" class="ojb_btn">${i18next.t('prev', { ns: 'comments' })}</button>
  9152. <button id="next-page-btn" class="ojb_btn">${i18next.t('next', { ns: 'comments' })}</button>
  9153. </div>
  9154. `);
  9155.  
  9156. let batchSize = 5;
  9157. let elements = $(".comments > .comment").slice(0, -1);
  9158. let start = 0;
  9159. let end = batchSize;
  9160. let currentPage = 1;
  9161. let displayedIndexes = []; // 存储已显示元素的索引
  9162.  
  9163. function showBatch(start, end) {
  9164. // 隐藏上一页
  9165. for (var i = 0; i < displayedIndexes.length; i++) {
  9166. elements.eq(displayedIndexes[i]).hide();
  9167. }
  9168.  
  9169. displayedIndexes = [];
  9170.  
  9171. // 显示当前页
  9172. elements.slice(start, end).each(function (index) {
  9173. $(this).show();
  9174. displayedIndexes.push(start + index);
  9175. });
  9176.  
  9177. // 更新页码和翻页按钮
  9178. $("#current-page").text(currentPage);
  9179. $("#total-pages").text(Math.ceil(elements.length / batchSize));
  9180.  
  9181. if (currentPage === 1) $("#prev-page-btn").hide();
  9182. else $("#prev-page-btn").show();
  9183.  
  9184. if (end >= elements.length) $("#next-page-btn").hide();
  9185. else $("#next-page-btn").show();
  9186. }
  9187.  
  9188. // 初始化
  9189. var commentID = null;
  9190. var pageURL = window.location.href;
  9191. if (pageURL.includes("#comment-")) {
  9192. // 带评论区锚点的链接
  9193. var startIndex = pageURL.lastIndexOf("#comment-") + 9;
  9194. commentID = pageURL.substring(startIndex);
  9195. var indexInComments = null;
  9196. $(".comments > .comment").each(function (index) {
  9197. $(this).find(".comment-table").each(function () {
  9198. var tableCommentID = $(this).attr("commentid");
  9199. if (tableCommentID === commentID) {
  9200. indexInComments = index;
  9201. return false;
  9202. }
  9203. });
  9204. });
  9205. let page = Math.ceil((indexInComments + 1) / batchSize);
  9206. currentPage = !page ? 1 : page;
  9207. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9208. setTimeout(function () {
  9209. window.location.href = pageURL;
  9210. }, 1000);
  9211. } else {
  9212. showBatch(0, batchSize);
  9213. }
  9214.  
  9215. $("#prev-page-btn").on("click", function () {
  9216. var itemsPerPage = parseInt($("#items-per-page").val());
  9217. start = (currentPage - 2) * itemsPerPage;
  9218. end = (currentPage - 1) * itemsPerPage;
  9219. currentPage--;
  9220. showBatch(start, end);
  9221. });
  9222.  
  9223. $("#next-page-btn").on("click", function () {
  9224. var itemsPerPage = parseInt($("#items-per-page").val());
  9225. start = currentPage * itemsPerPage;
  9226. end = (currentPage + 1) * itemsPerPage;
  9227. currentPage++;
  9228. showBatch(start, end);
  9229. });
  9230.  
  9231. $("#jump-btn").on("click", function () {
  9232. var inputPage = parseInt($("#jump-input").val());
  9233.  
  9234. if (inputPage >= 1 && inputPage <= Math.ceil(elements.length / parseInt($("#items-per-page").val()))) {
  9235. var itemsPerPage = parseInt($("#items-per-page").val());
  9236. start = (inputPage - 1) * itemsPerPage;
  9237. end = inputPage * itemsPerPage;
  9238. currentPage = inputPage; // 更新当前页码
  9239. showBatch(start, end);
  9240. }
  9241. });
  9242.  
  9243. $("#items-per-page").on("change", function () {
  9244. batchSize = parseInt($(this).val());
  9245. let page = Math.ceil(end / batchSize);
  9246. currentPage = !page ? 1 : page;
  9247. let maxPage = Math.ceil(elements.length / batchSize);
  9248. if (currentPage > maxPage) currentPage = maxPage;
  9249. showBatch((currentPage - 1) * batchSize, currentPage * batchSize);
  9250. });
  9251. }
  9252.  
  9253. /**
  9254. * 题目页相关链接栏
  9255. */
  9256. class ProblemPageLinkbar {
  9257. constructor() {
  9258. this.containerElement = this.createToolbar();
  9259. this.commandInvoker = new CommandInvoker();
  9260. }
  9261.  
  9262. /**
  9263. * 创建工具栏
  9264. */
  9265. createToolbar() {
  9266. const toolbarElement = $("<div>").attr("id", "problemToolbar").insertBefore($(".problemindexholder"));
  9267. return new DOMContainer(toolbarElement);
  9268. }
  9269.  
  9270. /**
  9271. * 添加按钮
  9272. * @param {string} id 按钮id
  9273. * @param {string} url 按钮链接
  9274. * @param {string} text 按钮文字
  9275. * @param {JQueryObject} icon 按钮图标
  9276. * @param {string} iconHeight 图标高度
  9277. * @returns {object} 按钮对象
  9278. */
  9279. addLinkButton(id, url, text, icon = $('<div>'), iconHeight = "22px") {
  9280. const linkElement = $("<a>")
  9281. .attr("href", url)
  9282. .attr("target", "_blank")
  9283. .addClass("ojb_btn")
  9284. .attr("id", id);
  9285.  
  9286. linkElement.append(icon);
  9287. icon.css("height", iconHeight);
  9288.  
  9289. const textSpan = $("<span>").html(text);
  9290. linkElement.append(textSpan);
  9291.  
  9292. this.commandInvoker.execute(new AddElementCommand(this.containerElement, linkElement));
  9293. return {
  9294. element: linkElement,
  9295. text: textSpan,
  9296. icon: icon
  9297. };
  9298. }
  9299.  
  9300. /**
  9301. * 更新链接
  9302. * @param {object} button 按钮对象
  9303. * @param {string} url 按钮链接
  9304. */
  9305. updateUrl(button, url) {
  9306. button.element.attr("href", url);
  9307. }
  9308.  
  9309. /**
  9310. * 更新文字
  9311. * @param {object} button 按钮对象
  9312. * @param {string} text 按钮文字
  9313. */
  9314. updateText(button, text) {
  9315. button.text.html(text);
  9316. }
  9317.  
  9318. /**
  9319. * 设置文字为粗体
  9320. * @param {object} button 按钮对象
  9321. */
  9322. setBold(button) {
  9323. button.text.css("font-weight", "bold");
  9324. }
  9325.  
  9326. /**
  9327. * 更新图标
  9328. * @param {object} button 按钮对象
  9329. * @param {JQueryObject} icon 按钮图标
  9330. * @param {string} iconHeight 图标高度
  9331. */
  9332. updateIcon(button, icon, iconHeight = "16px") {
  9333. button.icon.remove();
  9334. button.text.prepend(icon);
  9335. icon.css("height", iconHeight);
  9336. button.icon = icon;
  9337. }
  9338.  
  9339. /**
  9340. * 添加类
  9341. * @param {object} button 按钮对象
  9342. * @param {string} className 类名
  9343. */
  9344. addClass(button, className) {
  9345. button.element.addClass(className);
  9346. }
  9347.  
  9348. /**
  9349. * 禁用链接按钮
  9350. * @param {object} button 按钮对象
  9351. */
  9352. disableButton(button) {
  9353. button.element.addClass("disabled");
  9354. }
  9355.  
  9356. /**
  9357. * 启用链接按钮
  9358. * @param {object} button 按钮对象
  9359. */
  9360. enableButton(button) {
  9361. button.element.removeClass("disabled");
  9362. }
  9363. }
  9364.  
  9365. /**
  9366. * 获取题目的id
  9367. * @param {String} url 题目的链接
  9368. * @returns 题目的id,形如2000A
  9369. */
  9370. function getProblemId(url) {
  9371. const regexMap = new Map([
  9372. ['/contest/', /\/contest\/(\d+)\/problem\/([A-Za-z\d]+)/],
  9373. ['/problemset/', /\/problemset\/problem\/(\d+)\/([A-Za-z\d]+)/],
  9374. ['/gym/', /\/gym\/(\d+)\/problem\/([A-Za-z\d]+)/],
  9375. ]);
  9376.  
  9377. let regex = null;
  9378. for (let [key, value] of regexMap) {
  9379. if (url.includes(key)) {
  9380. regex = value;
  9381. break;
  9382. }
  9383. }
  9384.  
  9385. if (!regex) return '';
  9386.  
  9387. const matchResult = url.match(regex);
  9388. return matchResult && matchResult.length >= 3 ? `${matchResult[1]}${matchResult[2]}` : '';
  9389. };
  9390.  
  9391. /**
  9392. * 跳转到洛谷
  9393. * @param {ProblemPageLinkbar} problemToolbar
  9394. */
  9395. async function CF2luogu(problemToolbar) {
  9396. const url = window.location.href;
  9397. const problemId = getProblemId(url);
  9398. const luoguButton = problemToolbar.addLinkButton(
  9399. "luoguButton",
  9400. "https://www.luogu.com.cn/",
  9401. i18next.t('state.loading', { ns: 'button' }),
  9402. $("<img>").attr("src", "https://cdn.luogu.com.cn/fe/logo.png")
  9403. );
  9404.  
  9405. const checkLinkExistence = async (url) => {
  9406. return OJB_promiseRetryWrapper(async () => {
  9407. const response = await OJB_GMRequest({
  9408. method: "GET",
  9409. url
  9410. });
  9411. return !response.responseText.match(/出错了/g);
  9412. }, {
  9413. maxRetries: 3,
  9414. retryInterval: 1000
  9415. });
  9416. };
  9417.  
  9418. const LuoguUrl = `https://www.luogu.com.cn/problem/CF${problemId}`;
  9419. try {
  9420. const result = await checkLinkExistence(LuoguUrl);
  9421. if (problemId && result) {
  9422. problemToolbar.updateText(luoguButton, "");
  9423. problemToolbar.updateUrl(luoguButton, LuoguUrl);
  9424. } else {
  9425. problemToolbar.updateText(luoguButton, i18next.t('state.404', { ns: 'button' }));
  9426. problemToolbar.disableButton(luoguButton);
  9427. }
  9428. } catch (error) {
  9429. if (error instanceof OJB_GMError && error.type == "error") {
  9430. problemToolbar.updateText(luoguButton, i18next.t('state.netError', { ns: 'button' }));
  9431. problemToolbar.disableButton(luoguButton);
  9432. }
  9433. }
  9434. }
  9435.  
  9436. /**
  9437. * 跳转到 Virtual Judge
  9438. * @param {ProblemPageLinkbar} problemToolbar
  9439. */
  9440. async function CF2vjudge(problemToolbar) {
  9441. const url = window.location.href;
  9442. const problemId = getProblemId(url);
  9443. const vjudgeButton = problemToolbar.addLinkButton(
  9444. "vjudgeButton",
  9445. "https://vjudge.net/",
  9446. i18next.t('state.loading', { ns: 'button' }),
  9447. $("<img>").attr("src", "https://aowuucdn.oss-accelerate.aliyuncs.com/vjudge.ico")
  9448. );
  9449.  
  9450. const checkLinkExistence = async (url) => {
  9451. return OJB_promiseRetryWrapper(async () => {
  9452. const response = await OJB_GMRequest({
  9453. method: "HEAD",
  9454. url: url,
  9455. });
  9456. if (response.status >= 200 && response.status < 300) return true;
  9457. else if (response.status == 404) return false;
  9458. else throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9459. }, {
  9460. maxRetries: 3,
  9461. retryInterval: 1000
  9462. });
  9463. };
  9464.  
  9465. const VjudgeUrl = OJBetter.typeOfPage.is_gym ?
  9466. `https://vjudge.net/problem/Gym-${problemId}` :
  9467. `https://vjudge.net/problem/CodeForces-${problemId}`;
  9468. try {
  9469. const result = await checkLinkExistence(VjudgeUrl);
  9470. if (problemId && result) {
  9471. problemToolbar.updateText(vjudgeButton, "VJudge");
  9472. problemToolbar.updateUrl(vjudgeButton, VjudgeUrl);
  9473. } else {
  9474. problemToolbar.updateText(vjudgeButton, i18next.t('state.404', { ns: 'button' }));
  9475. problemToolbar.disableButton(vjudgeButton);
  9476. }
  9477. } catch (error) {
  9478. if (error instanceof OJB_GMError && error.type == "error") {
  9479. problemToolbar.updateText(vjudgeButton, i18next.t('state.netError', { ns: 'button' }));
  9480. problemToolbar.disableButton(vjudgeButton);
  9481. }
  9482. }
  9483. }
  9484.  
  9485. // RatingClass
  9486. const ratingClassMap = {
  9487. NaN: "rating_by_clist_colorNaN",
  9488. 0: "rating_by_clist_color0",
  9489. 1200: "rating_by_clist_color1",
  9490. 1400: "rating_by_clist_color2",
  9491. 1600: "rating_by_clist_color3",
  9492. 1900: "rating_by_clist_color4",
  9493. 2100: "rating_by_clist_color5",
  9494. 2300: "rating_by_clist_color6",
  9495. 2400: "rating_by_clist_color7",
  9496. 2600: "rating_by_clist_color8",
  9497. 3000: "rating_by_clist_color9"
  9498. };
  9499. const cssMap = {
  9500. "rating_by_clist_colorNaN": "#cccccc",
  9501. "rating_by_clist_color0": "#808080",
  9502. "rating_by_clist_color1": "#73e473",
  9503. "rating_by_clist_color2": "#77ddbb",
  9504. "rating_by_clist_color3": "#aaaaff",
  9505. "rating_by_clist_color4": "#ff88ff",
  9506. "rating_by_clist_color5": "#ffcc88",
  9507. "rating_by_clist_color6": "#ffbb55",
  9508. "rating_by_clist_color7": "#ff7777",
  9509. "rating_by_clist_color8": "#ff3333",
  9510. "rating_by_clist_color9": "#aa0000"
  9511. };
  9512. // TODO 7
  9513. /**
  9514. * clist 访问有效性检查
  9515. * @param {boolean} onlyCookie 是否只检查Cookie
  9516. * @returns {Promise<boolean>} 是否有效
  9517. */
  9518. async function validateClistConnection(onlyCookie = false) {
  9519. const clistApiUrl = "https://clist.by:443/api/v4/contest/?limit=1&resource_id=1";
  9520. const requestOptions = {
  9521. method: "GET",
  9522. url: clistApiUrl,
  9523. timeout: 5000,
  9524. };
  9525.  
  9526. // 尝试发送请求
  9527. async function tryRequest(options) {
  9528. try {
  9529. const response = await OJB_GMRequest(options);
  9530. if (response.status === 200) {
  9531. return { ok: true };
  9532. } else if (response.status === 401) {
  9533. throw new Error('unauthorized');
  9534. } else if (response.status === 404) {
  9535. throw new Error('not_found');
  9536. } else {
  9537. throw new Error('other_error');
  9538. }
  9539. } catch (error) {
  9540. console.warn(`Error accessing clist.by: ${error.message}`);
  9541. return { ok: false, error: error.message };
  9542. }
  9543. }
  9544.  
  9545. // 尝试携带Key发送请求
  9546. let result = await tryRequest(requestOptions);
  9547. if (!onlyCookie && !result.ok) {
  9548. requestOptions.headers = { "Authorization": OJBetter.clist.authorization };
  9549. result = await tryRequest(requestOptions);
  9550. }
  9551.  
  9552. // 根据结果显示错误信息
  9553. if (!result.ok) {
  9554. let errorType = result.error;
  9555. const loadingMessage = new LoadingMessage();
  9556. let state;
  9557. if (errorType === 'not_found') {
  9558. state = i18next.t('error.clist.404', { ns: 'alert' });
  9559. } else if (errorType === 'unauthorized') {
  9560. state = i18next.t('error.clist.cookie', { ns: 'alert' });
  9561. } else {
  9562. state = i18next.t('error.clist.other', { ns: 'alert' });
  9563. }
  9564. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${state}`, 'error');
  9565. }
  9566. return result.ok;
  9567. }
  9568.  
  9569. /**
  9570. * 创建Rating相关css
  9571. * @param {boolean} [hasBorder=true] 是否有边框
  9572. */
  9573. function creatRatingCss(hasBorder = true) {
  9574. const defaultBorderColor = '#dcdfe6';
  9575. let dynamicCss = "";
  9576. let hoverSelector = OJBetter.clist.ratingHidden ? ":hover" : "";
  9577. for (let cssClass in cssMap) {
  9578. dynamicCss += `a.${cssClass}${hoverSelector}, a.${cssClass}${hoverSelector}:link {\n`;
  9579. let borderColor = hasBorder ? cssMap[cssClass] : defaultBorderColor;
  9580. dynamicCss += ` color: ${cssMap[cssClass]} ${OJBetter.clist.ratingHidden ? "!important" : ""};\n`;
  9581. dynamicCss += `}\n`;
  9582. }
  9583. GM_addStyle(dynamicCss);
  9584. if (OJBetter.clist.ratingHidden) {
  9585. GM_addStyle(`
  9586. #clistButton {
  9587. color: #ffffff00;
  9588. }
  9589. `);
  9590. }
  9591. }
  9592.  
  9593. /**
  9594. * 模拟clist网页访问获取rating
  9595. * @param {string} problem 题目名称
  9596. * @param {string} problem_url 题目链接
  9597. * @param {string} contest 比赛名称
  9598. * @returns {Promise<{rating: number, problem: string}>} 题目难度
  9599. */
  9600. async function getRatingFromHTML(problem, problem_url, contest = null) {
  9601. // 去除题目名称中的括号,以及首尾的空白符
  9602. problem = problem.replace(/\([\s\S]*?\)/g, '').trim();
  9603.  
  9604. return OJB_promiseRetryWrapper(async () => {
  9605. const queryString = `search=${encodeURIComponent(problem)}&resource=1`;
  9606. const response = await OJB_GMRequest({
  9607. method: 'GET',
  9608. url: `https://clist.by/problems/?${queryString}`,
  9609. });
  9610.  
  9611. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9612. const html = response.responseText;
  9613. const cleanedHtml = html.replace(/src=(.|\s)*?"/g, '');
  9614. const parser = new DOMParser();
  9615. const doc = parser.parseFromString(cleanedHtml, 'text/html');
  9616. const trs = doc.querySelectorAll('table tbody tr');
  9617.  
  9618. for (let tr of trs) {
  9619. const rating = tr.querySelector('.problem-rating-column').textContent.trim();
  9620. const link = OJB_cleanLink(tr.querySelector('.problem-name-column a:nth-of-type(2)')?.href);
  9621.  
  9622. if (link === problem_url || link === problem_url + '/') {
  9623. return {
  9624. rating: parseInt(rating),
  9625. problem: problem
  9626. };
  9627. } else if (contest !== null) {
  9628. const contestTitles = [...tr.querySelectorAll('.problem-name-column .pull-right a[title], .problem-name-column .pull-right span[title]')].map(el => el.title);
  9629. if (contestTitles.includes(contest)) {
  9630. return {
  9631. rating: parseInt(rating),
  9632. problem: problem
  9633. };
  9634. }
  9635. }
  9636. }
  9637. console.warn(`No data found for the question: ${problem}`);
  9638. }, {
  9639. maxRetries: 3,
  9640. retryInterval: 500
  9641. });
  9642. }
  9643.  
  9644. /**
  9645. * 从clist API获取题目的rating
  9646. * @param {string} problem_name 题目名
  9647. * @param {string} problem_url 题目链接
  9648. * @returns {Promise<number>} 题目rating
  9649. *
  9650. * 使用两个Map对象来存储和快速访问题目信息:
  9651. * - problemsMap: 通过题目的URL作为键来存储题目信息。
  9652. * - nameMap: 通过题目的名称作为键来存储题目信息。
  9653. *
  9654. * 每个题目信息是一个对象,包含以下属性:
  9655. * @typedef {Object} ProblemInfo
  9656. * @property {string} name 题目名称
  9657. * @property {string} url 题目URL
  9658. * @property {number} rating 题目评分,如果没有评分信息则为NaN
  9659. */
  9660. async function getRatingFromApi_problem(problem_name, problem_url) {
  9661. return OJB_promiseRetryWrapper(async () => {
  9662. const response = await OJB_GMRequest({
  9663. method: "GET",
  9664. url: `https://clist.by:443/api/v4/problem/?name=${encodeURIComponent(problem_name)}&resource__regex=codeforces`,
  9665. headers: { "Authorization": OJBetter.clist.authorization }
  9666. });
  9667.  
  9668. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9669. let data = JSON.parse(response.responseText);
  9670. /**
  9671. * 使用题目的URL作为键来存储题目信息。
  9672. * @type {Map<string, ProblemInfo>}
  9673. */
  9674. let problemsMap = new Map();
  9675.  
  9676. /**
  9677. * 使用题目的名称作为键来存储题目信息。
  9678. * @type {Map<string, ProblemInfo>}
  9679. */
  9680. let nameMap = new Map();
  9681.  
  9682. data.objects.forEach(problem => {
  9683. /** @type {ProblemInfo} 题目信息*/
  9684. let problemInfo = {
  9685. name: problem.name,
  9686. url: problem.url,
  9687. rating: problem.rating ? problem.rating : NaN
  9688. };
  9689. problemsMap.set(OJB_cleanLink(problem.url), problemInfo);
  9690. nameMap.set(problem.name, problemInfo);
  9691. });
  9692.  
  9693. if (problemsMap.has(problem_url)) {
  9694. return problemsMap.get(problem_url).rating;
  9695. } else if (nameMap.has(problem_name)) {
  9696. return nameMap.get(problem_name).rating;
  9697. } else {
  9698. console.warn('Problem not found in the response');
  9699. }
  9700. }, {
  9701. maxRetries: 5,
  9702. retryInterval: 1000
  9703. });
  9704. }
  9705.  
  9706. /**
  9707. * 获取字符串中的关键词列表
  9708. * @param {string} text 字符串文本
  9709. * @returns {array<string>} 返回关键词列表
  9710. */
  9711. function getKeywords(text) {
  9712. // 定义要过滤掉的高频词
  9713. const highFrequencyWords = ['Educational', 'Codeforces', 'Round', 'Div'];
  9714.  
  9715. // 使用正则表达式替换掉特殊符号(保留空格以便分词)
  9716. const sanitizedText = text.replace(/[^\w\s]|_/g, '').replace(/\s+/g, ' ');
  9717.  
  9718. // 将字符串拆分为单词数组
  9719. const words = sanitizedText.split(' ');
  9720.  
  9721. // 过滤掉高频词和空字符串
  9722. const filteredWords = words.filter(word => {
  9723. return word && highFrequencyWords.indexOf(word) === -1;
  9724. });
  9725.  
  9726. // 返回关键词列表
  9727. return filteredWords;
  9728. }
  9729.  
  9730. /**
  9731. * 根据关键词从 Clist API 中获取实际比赛名称
  9732. * @param {string} contestName 比赛名
  9733. * @param {string} contestUrl 比赛链接
  9734. * @returns {string|null} 该比赛在Clist中的实际名字
  9735. */
  9736. async function getContestNameFromApi(contestName, contestUrl) {
  9737. return OJB_promiseRetryWrapper(async () => {
  9738. const options = {
  9739. method: "GET",
  9740. url: `https://clist.by:443/api/v4/contest/?resource_id=1&event__regex=${encodeURIComponent(contestName)}`,
  9741. headers: {
  9742. "Authorization": OJBetter.clist.authorization
  9743. }
  9744. };
  9745.  
  9746. let response = await OJB_GMRequest(options);
  9747.  
  9748. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9749.  
  9750. let data = JSON.parse(response.responseText);
  9751. let objects = data.objects;
  9752.  
  9753. if (objects.length > 0) {
  9754. for (const contest of objects) {
  9755. const href = contest.href.replace(/\/contests\//i, '/contest/'); // 链接可能是contests而不是contest,换回来
  9756. if (OJB_cleanLink(href) == contestUrl) {
  9757. return contest.event;
  9758. }
  9759. }
  9760. }
  9761. return null;
  9762. }, {
  9763. maxRetries: 5,
  9764. retryInterval: 1000
  9765. });
  9766. }
  9767.  
  9768. /**
  9769. * 获取在clist中的实际比赛名称
  9770. * @param {string} contestName 待搜索的比赛名称
  9771. * @param {string} contestUrl 比赛的url
  9772. * @returns {Promise<string|null>} 在clist中的实际比赛名称,如果没有找到,则返回null
  9773. */
  9774. async function getActualContestName(contestName, contestUrl) {
  9775. // 首先尝试使用完整的比赛名称进行搜索
  9776. let actualContestName = await getContestNameFromApi(contestName, contestUrl);
  9777. if (actualContestName) {
  9778. return actualContestName;
  9779. }
  9780.  
  9781. // 如果使用完整名称没有找到,则尝试使用关键词进行搜索
  9782. const keywords = getKeywords(contestName);
  9783. const maxKeywordAttempts = 1; // 最多尝试到第几个关键词(因为Clist API有频率限制)
  9784. for (let i = 0; i < Math.min(keywords.length, maxKeywordAttempts); i++) {
  9785. actualContestName = await getContestNameFromApi(keywords[i], contestUrl);
  9786. if (actualContestName) {
  9787. return actualContestName;
  9788. }
  9789. }
  9790.  
  9791. // 如果全部尝试后仍没有找到,返回null
  9792. return null;
  9793. }
  9794.  
  9795. /**
  9796. * 从clist API获取比赛题目集的rating
  9797. * @param {string} contestName 比赛名
  9798. * @returns {Promise<Map<string, number>>} 题目rating
  9799. */
  9800. async function getRatingFromApi_contest(contestName, contestUrl) {
  9801. const actualContestName = await getActualContestName(contestName, contestUrl);
  9802. return OJB_promiseRetryWrapper(async () => {
  9803. const options = {
  9804. method: "GET",
  9805. url: `https://clist.by:443/api/v4/contest/?resource_id=1&with_problems=true&event=${encodeURIComponent(actualContestName)}`,
  9806. headers: {
  9807. "Authorization": OJBetter.clist.authorization
  9808. }
  9809. };
  9810.  
  9811. let response = await OJB_GMRequest(options);
  9812.  
  9813. if (!response.responseText) throw new OJB_GMError('network', 'An unknown network error occurred!', response);
  9814.  
  9815. let data = JSON.parse(response.responseText);
  9816. let objects = data.objects;
  9817. let problemsMap = new Map();
  9818.  
  9819. if (objects.length > 0 && objects[0].problems) {
  9820. objects[0].problems.forEach(problem => {
  9821. problemsMap.set(OJB_cleanLink(problem.url), problem.rating ? problem.rating : NaN);
  9822. });
  9823. }
  9824.  
  9825. return problemsMap;
  9826. }, {
  9827. maxRetries: 5,
  9828. retryInterval: 1000
  9829. });
  9830. }
  9831.  
  9832. /**
  9833. * 根据rating获取对应的颜色class名
  9834. * @param {number} rating 题目rating
  9835. * @returns {string} 颜色class名
  9836. */
  9837. function getClassNameByRating(rating) {
  9838. let className = "rating_by_clist_color9";
  9839. if (Number.isNaN(rating)) {
  9840. className = "rating_by_clist_colorNaN";
  9841. } else {
  9842. let keys = Object.keys(ratingClassMap);
  9843. for (let i = 0; i < keys.length; i++) {
  9844. if (rating < keys[i]) {
  9845. className = ratingClassMap[keys[i - 1]];
  9846. break;
  9847. }
  9848. }
  9849. }
  9850. return className;
  9851. }
  9852.  
  9853. /**
  9854. * problem题目页显示Rating
  9855. * @param {ProblemPageLinkbar} problemToolbar
  9856. * @returns {Promise<void>}
  9857. */
  9858. async function showRatingByClist_problem(problemToolbar) {
  9859. // 题目名
  9860. let problem = $('.header .title').eq(0).text().replace(/[\s\S]*?. /, '');
  9861. if (OJBetter.typeOfPage.is_acmsguru) problem = $('h4').eq(0).text().replace(/[\s\S]*?. /, '');
  9862.  
  9863. // 创建Rating按钮元素
  9864. creatRatingCss(false);
  9865. // TODO
  9866. const clistButton = problemToolbar.addLinkButton(
  9867. 'clistButton',
  9868. `https://clist.by/problems/?search=${problem}&resource=1&resource=64`,
  9869. i18next.t('state.wait', { ns: 'button' }),
  9870. $("<img>").attr("src", "https://clist.by/static/img/logo-48.png"),
  9871. "15px"
  9872. );
  9873.  
  9874. // 检测clist连接
  9875. if (!await validateClistConnection()) {
  9876. problemToolbar.updateText(clistButton, i18next.t('state.netError', { ns: 'button' }));
  9877. return;
  9878. }
  9879.  
  9880. // 题目链接
  9881. let problem_url = window.location.href;
  9882. if (problem_url.includes('/contest/')) {
  9883. problem_url = problem_url.replace(/\/contest\/(\d+)\/problem\/(\w+)[^\w]*/, '/contest/$1/problem/$2');
  9884. } else {
  9885. problem_url = problem_url.replace(/\/problemset\/problem\/(\d+)\/(\w+)/, '/contest/$1/problem/$2');
  9886. }
  9887. if (OJBetter.typeOfPage.is_mSite) problem_url = problem_url.replace(/\/\/(\w+).codeforces.com/, '//codeforces.com'); // 轻量站
  9888.  
  9889. // 比赛名
  9890. // let contest = $('#sidebar').children().first().find('.rtable th').first().text();
  9891.  
  9892. // rating
  9893. problemToolbar.updateText(clistButton, i18next.t('state.loading', { ns: 'button' }));
  9894. let rating = await getRatingFromApi_problem(problem, problem_url);
  9895. if (rating) {
  9896. let className = getClassNameByRating(rating);
  9897. problemToolbar.updateText(clistButton, rating);
  9898. problemToolbar.setBold(clistButton);
  9899. problemToolbar.addClass(clistButton, className);
  9900. } else {
  9901. problemToolbar.updateText(clistButton, i18next.t('state.404', { ns: 'button' }));
  9902. problemToolbar.disableButton(clistButton);
  9903. }
  9904. }
  9905.  
  9906. /**
  9907. * contest页显示Rating
  9908. * @returns {Promise<void>}
  9909. */
  9910. async function showRatingByClist_contest() {
  9911. // 创建Rating显示框
  9912. creatRatingCss();
  9913. let ratingBadges = {};
  9914. $('.datatable .id.left').each(function () {
  9915. let href = 'https://codeforces.com' + $(this).find('a').attr('href');
  9916. let badge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge">${i18next.t('state.wait', { ns: 'button' })}</a>`);
  9917. $(this).find('a').after(badge);
  9918. ratingBadges[href] = badge;
  9919. });
  9920.  
  9921. // 检测clist连接
  9922. if (!await validateClistConnection()) {
  9923. for (let href in ratingBadges) {
  9924. ratingBadges[href].text('error').addClass('ratingBadge_error');
  9925. }
  9926. return;
  9927. }
  9928.  
  9929. // 显示loading
  9930. for (let href in ratingBadges) {
  9931. ratingBadges[href].text(i18next.t('state.loading', { ns: 'button' })).addClass('ratingBadge_loading');
  9932. }
  9933.  
  9934. // 获取Rating
  9935. let contestName = $('#sidebar').children().first().find('.rtable th').first().text();
  9936. let contestUrl = OJB_cleanLink(window.location.href);
  9937. try {
  9938. let problemsMap = await getRatingFromApi_contest(contestName, contestUrl);
  9939.  
  9940. // 填充数据
  9941. for (let href in ratingBadges) {
  9942. if (problemsMap.has(href)) {
  9943. let rating = problemsMap.get(href);
  9944. let className = getClassNameByRating(rating);
  9945. ratingBadges[href].text(rating).addClass(className);
  9946. } else {
  9947. ratingBadges[href].text(i18next.t('state.404', { ns: 'button' })).addClass('ratingBadge_no');
  9948. }
  9949. }
  9950. } catch (error) {
  9951. // 填充数据
  9952. for (let href in ratingBadges) {
  9953. ratingBadges[href].text(i18next.t('state.netError', { ns: 'button' })).addClass('ratingBadge_no');
  9954. }
  9955. console.warn(error);
  9956. }
  9957. }
  9958.  
  9959. /**
  9960. * problemset页显示Rating
  9961. * @returns {Promise<void>}
  9962. */
  9963. async function showRatingByClist_problemset() {
  9964. creatRatingCss();
  9965. let ratingBadges = [];
  9966. const $problems = $('.problems');
  9967. const $trs = $problems.find('tbody tr:gt(0)');
  9968.  
  9969. // 先创建Rating显示框,并将关系存进数组ratingBadges
  9970. for (let i = 0; i < $trs.length; i++) {
  9971. const $tds = $($trs[i]).find('td');
  9972. const $firstDiv = $($tds[1]).find('div:first');
  9973. let problem = $firstDiv.text();
  9974. let problem_url = $firstDiv.find('a').attr('href');
  9975. problem_url = problem_url.replace(/^\/problemset\/problem\/(\d+)\/(\w+)/, 'https://codeforces.com/contest/$1/problem/$2');
  9976.  
  9977. const ratingBadge = OJB_safeCreateJQElement(`<a id="clistButton" class="ratingBadge"></a>`);
  9978. const rating = OJB_safeCreateJQElement(`<span class="rating">${i18next.t('state.wait', { ns: 'button' })}</span>`);
  9979. ratingBadge.append(rating);
  9980. $($tds[0]).find('a').after(ratingBadge);
  9981. ratingBadges.push({ ratingBadge, rating, problem, problem_url });
  9982. }
  9983.  
  9984. // 检测clist连接
  9985. if (!await validateClistConnection()) {
  9986. for (let i = 0; i < rating.length; i++) {
  9987. ratingBadges[i].rating.text(i18next.t('state.netError', { ns: 'button' }));
  9988. }
  9989. return;
  9990. }
  9991.  
  9992. // 每次只获取3个rating
  9993. for (let i = 0; i < ratingBadges.length; i += 3) {
  9994. const promises = [];
  9995. const endIndex = Math.min(i + 3, ratingBadges.length);
  9996.  
  9997. for (let j = i; j < endIndex; j++) {
  9998. const ratingBadge = ratingBadges[j];
  9999. // 显示请求中
  10000. ratingBadge.rating.text(i18next.t('state.loading', { ns: 'button' }));
  10001. promises.push(getRatingFromHTML(ratingBadge.problem, ratingBadge.problem_url).catch(error => console.warn(error)));
  10002. }
  10003.  
  10004. const results = await Promise.all(promises);
  10005.  
  10006. for (let j = i; j < endIndex; j++) {
  10007. const result = results[j - i];
  10008. const ratingBadge = ratingBadges[j];
  10009. if (result) {
  10010. let className = getClassNameByRating(result.rating);
  10011. ratingBadge.ratingBadge.addClass(className);
  10012. ratingBadge.rating.text(result.rating);
  10013. } else {
  10014. ratingBadge.rating.text(i18next.t('state.404', { ns: 'button' }));
  10015. }
  10016. }
  10017. }
  10018. }
  10019.  
  10020. /**
  10021. * cf赛制榜单重新着色
  10022. */
  10023. async function recolorStandings() {
  10024. function getColorValue(value) {
  10025. value = Math.max(0, Math.min(1, value));
  10026.  
  10027. const scale = chroma.scale(['#b71c1c', '#ff9800', '#ffc107', '#00aa00']).mode('lch').domain([0, 0.45, 0.7, 1]);
  10028. return scale(value).hex();
  10029. }
  10030. var maxScores = $('.standings tr:first th:nth-child(n+5)')
  10031. .map(function () {
  10032. return $(this).find('span').text();
  10033. })
  10034. .get();
  10035. $('.standings tr:not(:first):not(:last)').each(function () {
  10036. var thElements = $(this).find('td:nth-child(n+5)');
  10037. thElements.each(function (index) {
  10038. var spanElement = $(this).find('span:first');
  10039. var value = parseInt(spanElement.text());
  10040. if (value <= 0 || /[a-zA-Z]/.test(maxScores[index])) return;
  10041. var colorValue = getColorValue(value / maxScores[index]);
  10042. spanElement.css('color', colorValue);
  10043. });
  10044. });
  10045. }
  10046.  
  10047. /**
  10048. * 存放编辑器语言select的值与Monaco语言对应关系的map.
  10049. * @type {Object.<string, string>}
  10050. */
  10051. const value_monacoLanguageMap = {
  10052. "4": "pascal", "6": "php", "7": "python", "9": "csharp", "13": "perl", "20": "scala", "31": "python",
  10053. "32": "go", "34": "javascript", "36": "java", "40": "python", "41": "python", "43": "cpp",
  10054. "50": "cpp", "51": "pascal", "52": "cpp", "54": "cpp", "55": "javascript", "59": "cpp", "60": "java",
  10055. "61": "cpp", "65": "csharp", "67": "ruby", "70": "python", "73": "cpp", "74": "java", "75": "rust",
  10056. "77": "kotlin", "79": "csharp", "80": "cpp", "83": "kotlin", "87": "java"
  10057. };
  10058.  
  10059. /**
  10060. * 更新代码提交页的HTML
  10061. * @param {string} submitUrl 提交页面的URL
  10062. * @param {string} cacheKey 本地缓存的键名
  10063. * @returns {Promise<jQuery<HTMLElement>>} 返回 jQuery 包装的 HTML 元素
  10064. */
  10065. async function CloneOriginalHTML(submitUrl, cacheKey) {
  10066. return OJB_promiseRetryWrapper(async () => {
  10067. const response = await OJB_GMRequest({
  10068. method: 'GET',
  10069. url: submitUrl
  10070. });
  10071. const html = response.responseText;
  10072. const parser = new DOMParser();
  10073. const doc = parser.parseFromString(html, 'text/html');
  10074. const cloneHTML = $(doc.body).html();
  10075. localStorage.setItem(cacheKey, html);
  10076. return $(cloneHTML);
  10077. }, {
  10078. maxRetries: 5,
  10079. retryInterval: 1000,
  10080. errorHandler: (err) => {
  10081. console.error('A network error occurred while retrieving the HTML for the code submission page.', submitUrl);
  10082. }
  10083. });
  10084. }
  10085.  
  10086. /**
  10087. * 获取代码提交页的HTML元素
  10088. * @param {string} submitUrl
  10089. * @returns {Promise<jQuery>}
  10090. */
  10091. async function getSubmitHTML(submitUrl) {
  10092. const cacheKey = 'OJBetter_CloneOriginalHTML';
  10093. const cookieKey = 'OJBetter_CloneOriginalHTML_time';
  10094. if (OJB_getCookie(cookieKey) === '1') {
  10095. // 存在缓存
  10096. CloneOriginalHTML(submitUrl, cacheKey);
  10097. // 校验
  10098. let cloneHTML = $(localStorage.getItem(cacheKey));
  10099. if (cloneHTML.find('form.submit-form').length > 0) {
  10100. return cloneHTML;
  10101. } else {
  10102. // 存在错误,更新缓存
  10103. console.warn(`Cache error detected!\nattempting to update, cache destination submitUrl:\n${submitUrl}`);
  10104. return await CloneOriginalHTML(submitUrl, cacheKey);
  10105. }
  10106.  
  10107. } else {
  10108. // 没有缓存,更新
  10109. document.cookie = `${cookieKey}=1; path=/`;
  10110. return await CloneOriginalHTML(submitUrl, cacheKey);
  10111. }
  10112. }
  10113.  
  10114. // 代码自动保存
  10115. async function saveCode(url, code) {
  10116. try {
  10117. await OJBetter.common.database.editorCode.put({ url, code });
  10118. return 'Code saved successfully';
  10119. } catch (error) {
  10120. throw new Error('Failed to save code');
  10121. }
  10122. }
  10123.  
  10124. async function getCode(url) {
  10125. try {
  10126. const result = await OJBetter.common.database.editorCode.get(url);
  10127. return result ? result.code : null;
  10128. } catch (error) {
  10129. throw new Error('Failed to get code');
  10130. }
  10131. }
  10132.  
  10133. // 创建代码编辑调试表单元素
  10134. async function createCodeEditorForm(submitUrl, cloneHTML) {
  10135. // 表单
  10136. let formDiv = $('<form method="post" id="OJBetter_SubmitForm" class="input-output-copier"></form>');
  10137. $('.ttypography').after(formDiv);
  10138. formDiv.attr('action', submitUrl + "?csrf_token=" + OJBetter.common.cf_csrf_token);
  10139.  
  10140. // 顶部区域
  10141. let topDiv = OJB_safeCreateJQElement(`<div class="topDiv"></div>`);
  10142. let selectLang = cloneHTML.find('select[name="programTypeId"]'); // 语言选择
  10143. selectLang.css({ 'margin': '10px 0px' }).attr('id', 'programTypeId');
  10144. topDiv.append(selectLang);
  10145. let topRightDiv = OJB_safeCreateJQElement(`<div class="topRightDiv"></div>`);
  10146. topDiv.append(topRightDiv);
  10147. formDiv.append(topDiv);
  10148.  
  10149. // 问题选择/编号
  10150. let selectProblem = $('<input name="submittedProblemIndex" style="display:none;"></input>');
  10151. let problemCode;
  10152. if (OJBetter.typeOfPage.is_acmsguru) {
  10153. problemCode = $('h4').eq(0).text();
  10154. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  10155. problemCode = matchResult[0];
  10156. } else if (OJBetter.typeOfPage.is_problemset_problem) {
  10157. let match = window.location.href.match(/\/problem\/([0-9]+?)\/([A-Za-z0-9]+)/);
  10158. problemCode = match[1] + match[2];
  10159. selectProblem.attr('name', 'submittedProblemCode');
  10160. } else {
  10161. problemCode = $('.header .title').eq(0).text();
  10162. let matchResult = problemCode.match(/([A-Z0-9]+)/);
  10163. problemCode = matchResult[0];
  10164. }
  10165. selectProblem.val(problemCode);
  10166. formDiv.append(selectProblem);
  10167.  
  10168. // 隐藏的代码记录
  10169. let sourceDiv = $('<textarea id="sourceCodeTextarea" name="source" style="display: none;"></textarea>');
  10170. formDiv.append(sourceDiv);
  10171.  
  10172. // 代码编辑器
  10173. let editorDiv = $('<div id="OJBetter_editor"></div>');
  10174. formDiv.append(editorDiv);
  10175.  
  10176. // monaco
  10177. let monaco = $('<div id="OJBetter_monaco"></div>');
  10178. editorDiv.append(monaco);
  10179.  
  10180. // 自定义调试
  10181. let customTestDiv = OJB_safeCreateJQElement(`
  10182. <details id="customTestBlock">
  10183. <summary >${i18next.t('customTestBlock.title', { ns: 'codeEditor' })}</summary>
  10184. <div id="customTests" style="min-height: 30px;"></div>
  10185. <div id="control" style="display:flex;">
  10186. <div style="display: flex;margin: 5px;">
  10187. <input type="checkbox" id="onlyCustomTest"}><label for="onlyCustomTest">
  10188. ${i18next.t('customTestBlock.onlyCustom', { ns: 'codeEditor' })}
  10189. </label>
  10190. </div>
  10191. <div style="display: flex;margin: 5px;">
  10192. <input type="checkbox" id="DontShowDiff"}>
  10193. <label for="DontShowDiff">
  10194. ${i18next.t('customTestBlock.DontShowDiff', { ns: 'codeEditor' })}
  10195. </label>
  10196. </div>
  10197. <button type="button" id="addCustomTest">${i18next.t('customTestBlock.add', { ns: 'codeEditor' })}</button>
  10198. </div>
  10199. </details>
  10200. `)
  10201. formDiv.append(customTestDiv);
  10202.  
  10203. // 调试/提交
  10204. let submitDiv = $('<div id="OJBetter_submitDiv"></div>');
  10205. let CompilerArgsInput = $('<input type="text" id="CompilerArgsInput">');
  10206. submitDiv.append(CompilerArgsInput);
  10207.  
  10208. let runButton = OJB_safeCreateJQElement(`
  10209. <button type="button" id="RunTestButton" class="ojb_btn ojb_btn_popover top">
  10210. <i class="iconfont">&#xe6c1;</i>
  10211. <span class="popover_content">${i18next.t('runTestButton.initial', { ns: 'codeEditor' })}</span>
  10212. </button>
  10213. `);
  10214. let submitButton = OJB_safeCreateJQElement(`
  10215. <button id="SubmitButton" class="ojb_btn ojb_btn_popover top" type="submit">
  10216. <i class="iconfont">&#xe633;</i>
  10217. <span class="popover_content">${i18next.t('submitButton', { ns: 'codeEditor' })}</span>
  10218. </button>
  10219. `);
  10220. if (OJBetter.monaco.setting.submitButtonPosition == "bottom") {
  10221. // 添加测试/提交按钮到底部
  10222. submitDiv.append(runButton);
  10223. submitDiv.append(submitButton);
  10224. }
  10225.  
  10226. formDiv.append(submitDiv);
  10227. let CompilerSetting = OJB_safeCreateJQElement(`
  10228. <div id="CompilerSetting"></div>
  10229. `);
  10230. formDiv.append(CompilerSetting);
  10231. let statePanel = OJB_safeCreateJQElement(`
  10232. <div id="statePanel"></div>
  10233. `);
  10234. formDiv.append(statePanel);
  10235.  
  10236. let from = {
  10237. formDiv: formDiv,
  10238. selectLang: selectLang,
  10239. topRightDiv: topRightDiv,
  10240. sourceDiv: sourceDiv,
  10241. editorDiv: editorDiv,
  10242. monaco: monaco,
  10243. runButton: runButton,
  10244. submitButton: submitButton,
  10245. submitDiv: submitDiv,
  10246. CompilerSetting: CompilerSetting,
  10247. statePanel: statePanel
  10248. };
  10249. return from;
  10250. }
  10251.  
  10252. // 解析ace格式的补全规则(acwing)
  10253. function parseAceCompleter(rules, range) {
  10254. const suggestions = [];
  10255. if (rules && rules.templates && rules.templates.items) {
  10256. const items = rules.templates.items;
  10257. for (let i = 0; i < items.length; i++) {
  10258. const item = items[i];
  10259. const parts = item.caption.split(' ');
  10260. for (let i = 0; i < parts.length; i++) {
  10261. if (item.value.startsWith(parts[i])) {
  10262. item.value = item.value.replace(parts[i], parts.slice(0, i + 1).join(' '));
  10263. break;
  10264. }
  10265. }
  10266. const completionItem = {
  10267. label: item.caption,
  10268. kind: monaco.languages.CompletionItemKind.Function,
  10269. insertText: item.value,
  10270. range: range
  10271. };
  10272. suggestions.push(completionItem);
  10273. }
  10274. }
  10275. return { suggestions };
  10276. }
  10277.  
  10278. // 解析monaco格式的补全规则
  10279. function parseMonacoCompleter(rules, range) {
  10280. const suggestions_ = [];
  10281. if (rules && rules.suggestions) {
  10282. const suggestion = rules.suggestions;
  10283. for (let i = 0; i < rules.suggestions.length; i++) {
  10284. const item = suggestion[i];
  10285. const completionItem = {
  10286. ...item,
  10287. range: range
  10288. };
  10289. suggestions_.push(completionItem);
  10290. }
  10291. }
  10292. return { suggestions: suggestions_ };
  10293. }
  10294.  
  10295. /**
  10296. * 创建monaco编辑器的一个实例
  10297. */
  10298. async function createMonacoEditor(language, form, support) {
  10299. // 判断monacoLoader是否加载完毕
  10300. await OJB_waitUntilTrue(() => OJBetter.monaco.loaderOnload);
  10301.  
  10302. /**
  10303. * 通用参数
  10304. */
  10305. var id = 0; // 协议中的id标识
  10306. var workspace = language + "_workspace";
  10307. var rootUri = OJBetter.monaco.lsp.workUri + "/" + workspace;
  10308. // 文件名
  10309. var InstanceID = OJB_getRandomNumber(8).toString();
  10310. var filename = language == "java" ? "hello/src/" + InstanceID : InstanceID;
  10311. // 后缀名
  10312. var fileExtension =
  10313. language === "cpp"
  10314. ? ".cpp"
  10315. : language === "python"
  10316. ? ".py"
  10317. : language === "java"
  10318. ? ".java"
  10319. : "";
  10320. var uri = rootUri + "/" + filename + fileExtension;
  10321. var initialized = false; // 是否已初始化
  10322. var serverInfo; // 服务器返回的支持信息
  10323. var model; // model
  10324. var OJBetter_monaco = {};
  10325. window.OJBetter_monaco = OJBetter_monaco; // 全局方法
  10326.  
  10327. /**
  10328. * 一些工具函数
  10329. */
  10330. // 将lsp格式的rang转换为Monaco格式
  10331. OJBetter_monaco.lspRangeToMonacoRange = function (range) {
  10332. const { start, end } = range;
  10333. return new monaco.Range(
  10334. start.line + 1,
  10335. start.character + 1,
  10336. end.line + 1,
  10337. end.character + 1
  10338. );
  10339. };
  10340. // 将Monaco格式的rang转为lsp格式
  10341. OJBetter_monaco.MonacoRangeTolspRange = function (range) {
  10342. return {
  10343. start: {
  10344. line: range.startLineNumber - 1,
  10345. character: range.startColumn - 1,
  10346. },
  10347. end: {
  10348. line: range.endLineNumber - 1,
  10349. character: range.endColumn - 1,
  10350. },
  10351. };
  10352. };
  10353. // 将Monaco格式的position转为lsp格式的
  10354. OJBetter_monaco.MonacoPositionTolspPosition = function (position) {
  10355. return {
  10356. line: position.lineNumber - 1,
  10357. character: position.column - 1,
  10358. };
  10359. };
  10360. // 将Monaco格式的severity转为lsp格式的
  10361. OJBetter_monaco.MonacoSeverityTolspSeverity = function (severity) {
  10362. switch (severity) {
  10363. case 8:
  10364. return 1;
  10365. case 1:
  10366. return 4;
  10367. case 2:
  10368. return 3;
  10369. case 4:
  10370. return 2;
  10371. default:
  10372. return severity;
  10373. }
  10374. };
  10375. // 将lsp格式的severity转为Monaco格式的
  10376. OJBetter_monaco.lspSeverityToMonacoSeverity = function (severity) {
  10377. switch (severity) {
  10378. case 1:
  10379. return 8;
  10380. case 4:
  10381. return 1;
  10382. case 3:
  10383. return 2;
  10384. case 2:
  10385. return 4;
  10386. default:
  10387. return severity;
  10388. }
  10389. };
  10390. // 收集Monaco数据中的rang数据
  10391. OJBetter_monaco.CollectRange = function (item) {
  10392. return {
  10393. startLineNumber: item.startLineNumber,
  10394. startColumn: item.startColumn,
  10395. endLineNumber: item.endLineNumber,
  10396. endColumn: item.endColumn,
  10397. };
  10398. };
  10399. // 收集Monaco position数据中的rang数据
  10400. OJBetter_monaco.CollectRangeByPosition = function (item) {
  10401. var word = model.getWordUntilPosition(item);
  10402. return {
  10403. startLineNumber: item.lineNumber,
  10404. endLineNumber: item.lineNumber,
  10405. startColumn: word.startColumn,
  10406. endColumn: word.endColumn,
  10407. };
  10408. };
  10409. // 将lsp格式的Edit转换为Monaco格式
  10410. OJBetter_monaco.lspEditToMonacoEdit = function (edit) {
  10411. const edits = [];
  10412.  
  10413. if (language == "python") {
  10414. for (const item1 of edit.documentChanges) {
  10415. for (const item2 of item1.edits) {
  10416. const newElement = {
  10417. textEdit: {
  10418. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10419. text: item2.newText,
  10420. },
  10421. resource: monaco.Uri.parse(item1.textDocument.uri),
  10422. versionId: model.getVersionId(),
  10423. };
  10424. edits.push(newElement);
  10425. }
  10426. }
  10427. } else if (language == "java") {
  10428. for (const item1 in edit.changes) {
  10429. edit.changes[item1].forEach((item2) => {
  10430. const newElement = {
  10431. textEdit: {
  10432. range: OJBetter_monaco.lspRangeToMonacoRange(item2.range),
  10433. text: item2.newText,
  10434. },
  10435. resource: uri,
  10436. versionId: model.getVersionId(),
  10437. };
  10438. edits.push(newElement);
  10439. });
  10440. }
  10441. } else {
  10442. for (const key in edit.changes) {
  10443. const arr = edit.changes[key];
  10444. for (const item of arr) {
  10445. const newElement = {
  10446. textEdit: {
  10447. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  10448. text: item.newText,
  10449. },
  10450. resource: monaco.Uri.parse(key),
  10451. versionId: model.getVersionId(),
  10452. };
  10453. edits.push(newElement);
  10454. }
  10455. }
  10456. }
  10457. return { edits: edits };
  10458. };
  10459.  
  10460. /**
  10461. * 实例化一个editor
  10462. */
  10463. uri = monaco.Uri.file(uri);
  10464. model = monaco.editor.createModel('', language, uri);
  10465. OJBetter.monaco.editor = monaco.editor.create(document.getElementById("OJBetter_monaco"), {
  10466. model: model,
  10467. rootUri: rootUri,
  10468. fontSize: 15,
  10469. tabSize: 4,
  10470. theme: OJBetter.basic.darkMode == "dark" ? "vs-dark" : "vs",
  10471. bracketPairColorization: {
  10472. enabled: true,
  10473. independentColorPoolPerBracketType: true,
  10474. },
  10475. automaticLayout: true,
  10476. lineNumbersMinChars: 3,
  10477. matchOnWordStartOnly: false,
  10478. wordWrap: "on",
  10479. wrappingIndent: "same",
  10480. glyphMargin: true,
  10481. formatOnType: true,
  10482. scrollbar: {
  10483. verticalScrollbarSize: 10,
  10484. horizontalScrollbarSize: 10,
  10485. alwaysConsumeMouseWheel: OJBetter.monaco.setting.alwaysConsumeMouseWheel
  10486. },
  10487. suggest: {
  10488. selectionMode: 'never' // 代码建议不自动选择
  10489. }
  10490. });
  10491.  
  10492. /**
  10493. * 添加快捷功能
  10494. */
  10495. (OJBetter_monaco.addShortCuts = async () => {
  10496. // 从配置信息更新字体大小
  10497. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(OJBetter.monaco.setting.fontsize) });
  10498.  
  10499. // 调整字体大小
  10500. let changeSize = OJB_safeCreateJQElement(`
  10501. <div class="ojb_btn ojb_btn_popover top">
  10502. <input type="number" id="fontSizeInput" value="${OJBetter.monaco.setting.fontsize}">
  10503. <span class="popover_content">${i18next.t('fontSizeInput', { ns: 'codeEditor' })}</span>
  10504. </div>`)
  10505. form.topRightDiv.append(changeSize);
  10506. changeSize.find('input#fontSizeInput').on('input', function () {
  10507. var size = $(this).val();
  10508. OJBetter.monaco.editor.updateOptions({ fontSize: parseInt(size) });
  10509. GM_setValue('editorFontSize', size);
  10510. });
  10511.  
  10512. // 全屏按钮
  10513. let fullscreenButton = OJB_safeCreateJQElement(`
  10514. <button type="button" class="ojb_btn ojb_btn_popover top">
  10515. <i class="iconfont">&#xe606;</i>
  10516. <span class="popover_content">${i18next.t('fullscreenButton', { ns: 'codeEditor' })}</span>
  10517. </button>
  10518. `);
  10519. form.topRightDiv.append(fullscreenButton);
  10520. fullscreenButton.on('click', enterFullscreen);
  10521.  
  10522. // 固定到底部按钮
  10523. let fixToBottomButton = OJB_safeCreateJQElement(`
  10524. <button type="button" class="ojb_btn ojb_btn_popover top">
  10525. <i class="iconfont">&#xe607;</i>
  10526. <span class="popover_content">${i18next.t('fixToBottomButton', { ns: 'codeEditor' })}</span>
  10527. </button>
  10528. `);
  10529. form.topRightDiv.append(fixToBottomButton);
  10530. fixToBottomButton.on('click', fixToBottom);
  10531.  
  10532. // 固定到右侧按钮
  10533. let fixToRightButton = OJB_safeCreateJQElement(`
  10534. <button type="button" class="ojb_btn ojb_btn_popover top">
  10535. <i class="iconfont">&#xe605;</i>
  10536. <span class="popover_content">${i18next.t('fixToRightButton', { ns: 'codeEditor' })}</span>
  10537. </button>
  10538. `);
  10539. form.topRightDiv.append(fixToRightButton);
  10540. fixToRightButton.on('click', fixToRight);
  10541.  
  10542. // 添加测试/提交按钮到顶部
  10543. if (OJBetter.monaco.setting.submitButtonPosition == "top") {
  10544. form.topRightDiv.append(form.runButton);
  10545. form.topRightDiv.append(form.submitButton);
  10546. }
  10547.  
  10548. // 选择记忆
  10549. if (!OJBetter.monaco.setting.position_initialized) {
  10550. OJBetter.monaco.setting.position_initialized = true; // 标记是否已经初始化过
  10551. if (OJBetter.monaco.setting.position == "full") {
  10552. fullscreenButton.click();
  10553. } else if (OJBetter.monaco.setting.position == "bottom") {
  10554. fixToBottomButton.click();
  10555. } else if (OJBetter.monaco.setting.position == "right") {
  10556. fixToRightButton.click();
  10557. }
  10558. }
  10559.  
  10560. // 禁用按钮
  10561. function disableButtons() {
  10562. fullscreenButton.prop("disabled", true);
  10563. fixToBottomButton.prop("disabled", true);
  10564. fixToRightButton.prop("disabled", true);
  10565. }
  10566.  
  10567. // 启用按钮
  10568. function enableButtons() {
  10569. fullscreenButton.prop("disabled", false);
  10570. fixToBottomButton.prop("disabled", false);
  10571. fixToRightButton.prop("disabled", false);
  10572. }
  10573.  
  10574. // 进入全屏
  10575. function enterFullscreen() {
  10576. let editor = $('#OJBetter_editor');
  10577. editor.addClass('fullscreen');
  10578.  
  10579. // 取消按钮
  10580. let cancelButton = OJB_safeCreateJQElement(`
  10581. <button type="button" class="ojb_btn ojb_btn_popover top primary exit_button_bottom">
  10582. <i class="iconfont">&#xe60b;</i>
  10583. <span class="popover_content">${i18next.t('exitFullscreenButton', { ns: 'codeEditor' })}</span>
  10584. </button>
  10585. `).on('click', () => exitFullscreen(cancelButton));
  10586. $('body').append(cancelButton);
  10587.  
  10588. disableButtons();
  10589. GM_setValue("monacoEditor_position", "full");
  10590. }
  10591.  
  10592. // 退出全屏
  10593. const exitFullscreen = (cancelButton) => {
  10594. let editor = $('#OJBetter_editor');
  10595. editor.removeClass('fullscreen');
  10596. cancelButton.remove();
  10597. enableButtons();
  10598. GM_setValue("monacoEditor_position", "initial");
  10599. };
  10600.  
  10601. // 固定到底部
  10602. function fixToBottom() {
  10603. let editor = $('#OJBetter_editor');
  10604. editor.addClass('bottom');
  10605.  
  10606. let halfHeight = $(window).height() * 0.5;
  10607. let blankSpace = $('<div>', {
  10608. 'class': 'blank-space',
  10609. 'style': 'height: ' + (halfHeight + 30) + 'px;'
  10610. });
  10611. $('body').append(blankSpace);
  10612.  
  10613. let cancelButton = OJB_safeCreateJQElement(`
  10614. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10615. <i class="iconfont">&#xe625;</i>
  10616. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10617. </button>
  10618. `).on('click', () => cancelFixingToBottom(cancelButton, blankSpace));
  10619. $('body').append(cancelButton);
  10620.  
  10621. disableButtons();
  10622. GM_setValue("monacoEditor_position", "bottom");
  10623. }
  10624.  
  10625. // 取消固定到底部
  10626. const cancelFixingToBottom = (cancelButton, blankSpace) => {
  10627. let editor = $('#OJBetter_editor');
  10628. editor.removeClass('bottom');
  10629. cancelButton.remove();
  10630. blankSpace.remove();
  10631. enableButtons();
  10632. GM_setValue("monacoEditor_position", "initial");
  10633. };
  10634.  
  10635. // 固定到右侧边栏
  10636. function fixToRight() {
  10637. const sidebar = $('#sidebar').hide();
  10638.  
  10639. // 添加样式
  10640. const styleElement = GM_addStyle(`
  10641. #body {
  10642. min-width: 50vw;
  10643. max-width: 50vw;
  10644. max-height: 100vh;
  10645. overflow-x: hidden;
  10646. overflow-y: auto;
  10647. padding: 1rem;
  10648. box-sizing: border-box;
  10649. }
  10650. body {
  10651. margin: 0px;
  10652. }
  10653. .content-with-sidebar {
  10654. margin-right: 0px !important;
  10655. }
  10656. .menu-list li {
  10657. margin-right: 0.5em;
  10658. }
  10659. .menu-list li a {
  10660. font-size: 1.4rem;
  10661. }
  10662. #OJBetter_editor{
  10663. height: 100vh;
  10664. width: 50vw;
  10665. }
  10666. `);
  10667.  
  10668. // 包装一层div
  10669. $('#body').wrap('<div id="right-side-wrapper" style="display:flex; max-width: 100vw; overflow: hidden;"></div>');
  10670. const blankSpace = $('<div>').appendTo('#right-side-wrapper');
  10671.  
  10672. // 移动编辑器
  10673. const editor = $('#OJBetter_editor').prependTo(blankSpace).addClass('right-side');
  10674.  
  10675. // 取消按钮
  10676. const cancelButton = OJB_safeCreateJQElement(`
  10677. <button type="button" class="ojb_btn ojb_btn_popover top enabled exit_button_bottom">
  10678. <i class="iconfont">&#xe625;</i>
  10679. <span class="popover_content">${i18next.t('cancelFixButton', { ns: 'codeEditor' })}</span>
  10680. </button>
  10681. `).on('click', () => cancelFixingToRight(sidebar, styleElement, editor, cancelButton)).appendTo('body');
  10682.  
  10683. disableButtons();
  10684. GM_setValue("monacoEditor_position", "right");
  10685.  
  10686. // 补丁:修复固定到右侧导致的样例元素.sample-test相关代码重复执行的问题(具体原因未查)
  10687. $('.sample-test').find('.title').each((i, e) => {
  10688. if ($(e).find('.input-output-copier').length > 1) {
  10689. $(e).find('.input-output-copier').first().remove();
  10690. }
  10691. });
  10692. darkModeStyleAdjustment();
  10693. }
  10694.  
  10695. const cancelFixingToRight = (sidebar, styleElement, editor, cancelButton) => {
  10696. sidebar.show();
  10697. // 移回来
  10698. editor.insertAfter(form.sourceDiv).removeClass('right-side');
  10699.  
  10700. // 移除包装
  10701. $('#body').unwrap();
  10702. cancelButton.remove();
  10703. styleElement.remove(); // 移除添加的样式
  10704.  
  10705. enableButtons();
  10706. GM_setValue("monacoEditor_position", "initial");
  10707. }
  10708.  
  10709. // 代码同步与保存
  10710. if (OJBetter.monaco.setting.autoMemoryCode) {
  10711. let nowUrl = window.location.href;
  10712. nowUrl = nowUrl.replace(/#/, ""); // 当页面存在更改时url会多出一个#,去掉
  10713. const code = await getCode(nowUrl);
  10714. if (code) {
  10715. OJBetter.monaco.editor.setValue(code); // 恢复代码
  10716. form.sourceDiv.val(code);
  10717. }
  10718. OJBetter.monaco.editor.onDidChangeModelContent(async () => {
  10719. // 将monaco editor的内容同步到sourceDiv
  10720. const code = OJBetter.monaco.editor.getValue();
  10721. form.sourceDiv.val(code);
  10722. await saveCode(nowUrl, code);
  10723. });
  10724. }
  10725. })();
  10726.  
  10727. /**
  10728. * 注册本地自动补全
  10729. */
  10730. (OJBetter_monaco.RegisterLocalComplet = async () => {
  10731. // 补全器注册函数
  10732. function registMyCompletionItemProvider(language, genre, rule) {
  10733. if (genre == "monaco") {
  10734. monaco.languages.registerCompletionItemProvider(language, {
  10735. provideCompletionItems: function (model, position) {
  10736. return parseMonacoCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10737. }
  10738. })
  10739. } else if (genre == "ace") {
  10740. monaco.languages.registerCompletionItemProvider(language, {
  10741. provideCompletionItems: function (model, position) {
  10742. return parseAceCompleter(rule, OJBetter_monaco.CollectRangeByPosition(position));
  10743. }
  10744. })
  10745. }
  10746. }
  10747.  
  10748. // 注册acwing cpp 模板
  10749. if (language == "cpp" && OJBetter.monaco.complet.cppCodeTemplate) {
  10750. try {
  10751. var acwing_cpp_code_completer = JSON.parse(GM_getResourceText("acwing_cpp_code_completer"));
  10752. registMyCompletionItemProvider('cpp', 'ace', acwing_cpp_code_completer);
  10753. } catch (error) {
  10754. console.error("Error registering acwing cpp template:", error);
  10755. }
  10756. }
  10757.  
  10758. // 注册自定义的补全
  10759. let complet_length = OJBetter.monaco.complet.customConfig.configurations.length;
  10760. if (complet_length > 0) {
  10761. for (let i = 0; i < complet_length; i++) {
  10762. let item = OJBetter.monaco.complet.customConfig.configurations[i];
  10763. if (item.isChoose && item.language == language) {
  10764. try {
  10765. let rule = await OJB_getExternalJSON(item.jsonUrl);
  10766. registMyCompletionItemProvider(item.language, item.genre, rule);
  10767. } catch (error) {
  10768. console.error(`Error registering custom completer for ${item.language}:`, error);
  10769. }
  10770. }
  10771. }
  10772. }
  10773. })();
  10774.  
  10775. if (!support || !OJBetter.monaco.lsp.enabled) { return; } // 如果不支持lsp,则到此为止
  10776.  
  10777. /**
  10778. * LSP连接状态指示
  10779. */
  10780. const lspStateButton = OJB_safeCreateJQElement(`
  10781. <div id="lspStateDiv" class="ojb_btn ojb_btn_popover top loading">
  10782. <i class="iconfont">&#xe658;</i>
  10783. <span class="popover_content">${i18next.t('lsp.connect', { ns: 'codeEditor' })}</span>
  10784. </div>
  10785. `).on('click', () => {
  10786. OJB_showModal(LSPLogDiv);
  10787. LSPLogDiv.show();
  10788. });
  10789. form.topRightDiv.prepend(lspStateButton);
  10790.  
  10791. const LSPLogDiv = OJB_safeCreateJQElement(`
  10792. <dialog id="LSPLog" style="display: none;">
  10793. <button class="ojb_btn">${i18next.t('close', { ns: 'common' })}</button>
  10794. <div id="LSPLogList" style="overflow: auto;"></div>
  10795. <dialog>`);
  10796. $('body').append(LSPLogDiv);
  10797.  
  10798. const LSPLogList = $('<ul></ul>');
  10799. $('#LSPLogList').append(LSPLogList);
  10800.  
  10801. const closeButton = LSPLogDiv.find('button');
  10802. closeButton.on('click', function () {
  10803. OJB_closeModal(LSPLogDiv);
  10804. });
  10805.  
  10806. /**
  10807. * 推送新的消息到LSP日志中
  10808. * @param {'error' | 'warn' | 'info'} status
  10809. * @param {string} msg
  10810. * @param {boolean} data
  10811. */
  10812. function pushLSPLogMessage(status, msg, data) {
  10813. var li = $('<li>').text('[' + new Date().toLocaleString() + '] ' + msg);
  10814. if (status === 'error') {
  10815. li.attr('style', 'color: #f44336;');
  10816. } else if (status === 'warn') {
  10817. li.attr('style', 'color: #ff9800;');
  10818. } else if (status === 'info') {
  10819. li.attr('style', 'color: #616161;');
  10820. }
  10821. if (data) {
  10822. var jsonText = JSON.stringify(data, null, 2);
  10823. var details = $('<details>');
  10824. var summary = $('<summary>').text('Data');
  10825. var pre = $('<pre>').text(jsonText);
  10826. details.append(summary, pre);
  10827. li.append(details);
  10828. }
  10829. LSPLogList.append(li);
  10830. }
  10831.  
  10832. /**
  10833. * 添加状态底栏
  10834. */
  10835. var statusBar = $('<div id="OJBetter_statusBar">');
  10836. form.editorDiv.append(statusBar);
  10837.  
  10838. /**
  10839. * languageSocket
  10840. */
  10841. var url = OJBetter.monaco.lsp.socketUrl;
  10842. var languageSocket = new WebSocket(url + language);
  10843. OJBetter.monaco.lsp.socket.push(languageSocket);
  10844. var languageSocketState = false;
  10845. var responseHandlers = new Map(); // 映射表,需要等待返回数据的请求 -> 对应的事件触发函数
  10846.  
  10847. languageSocket.onopen = () => {
  10848. languageSocketState = true;
  10849. lspStateButton.setButtonPopover(i18next.t('lsp.waitingAnswer', { ns: 'codeEditor' }));
  10850. pushLSPLogMessage("info", `languageSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10851. };
  10852. languageSocket.onmessage = (event) => {
  10853. const message = JSON.parse(event.data);
  10854. if (message.id === 0 && message.result) {
  10855. // 初始化完成
  10856. lspStateButton.setButtonState('success', i18next.t('lsp.connected', { ns: 'codeEditor' }));
  10857. pushLSPLogMessage("info", `Initialization ${i18next.t('lsp.state.finished', { ns: 'logMessage' })}`);
  10858. serverInfo = message.result; // 存下服务器支持信息
  10859. OJBetter_monaco.openDocRequest(); // 打开文档
  10860. if (!OJBetter.monaco.setting.language.includes(language)) {
  10861. OJBetter.monaco.setting.language.push(language);
  10862. OJBetter_monaco.RegistrationAfterInit(); // 注册语言及功能
  10863. } else {
  10864. location.reload(); // 这里有问题,先贴个补丁
  10865. }
  10866. OJBetter_monaco.PassiveReceiveHandler(); // 注册被动接收函数
  10867. } else if (message.id === 0 && message.error) {
  10868. pushLSPLogMessage("warn", `Initialization ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`);
  10869. } else if (message.id !== undefined && responseHandlers.has(message.id)) {
  10870. // 如果收到带有id字段的消息,则回传给对应的事件触发函数
  10871. const handler = responseHandlers.get(message.id);
  10872. if (handler) {
  10873. handler(message);
  10874. responseHandlers.delete(message.id); // 删除已处理的事件触发函数
  10875. }
  10876. } else if (message.method == "textDocument/publishDiagnostics") {
  10877. // 接收代码诊断推送
  10878. OJBetter_monaco.updateMarkers(message);
  10879. } else if (message.method == "workspace/applyEdit") {
  10880. // 应用服务器推送的更改
  10881. OJBetter_monaco.applyEdit(message);
  10882. }
  10883. };
  10884. languageSocket.onerror = (error) => {
  10885. pushLSPLogMessage("error", `languageSocket ${i18next.t('lsp.state.error', { ns: 'logMessage' })}`, error);
  10886. console.warn(`Error connecting to languageSocket: ${error}`)
  10887. };
  10888. languageSocket.onclose = (event) => {
  10889. languageSocketState = false;
  10890. lspStateButton.setButtonState('error', i18next.t('lsp.error', { ns: 'codeEditor' }));
  10891. pushLSPLogMessage("warn", `languageSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`);
  10892. };
  10893.  
  10894. /**
  10895. * 等待LanguageSocketState
  10896. */
  10897. async function waitForLanguageSocketState() {
  10898. return new Promise((resolve) => {
  10899. const checkInitialized = () => {
  10900. if (languageSocketState) {
  10901. resolve();
  10902. } else {
  10903. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10904. }
  10905. };
  10906. checkInitialized();
  10907. });
  10908. }
  10909.  
  10910. // 等待lsp响应初始化结果
  10911. async function waitForInitialized() {
  10912. return new Promise((resolve) => {
  10913. const checkInitialized = () => {
  10914. if (initialized) {
  10915. resolve();
  10916. } else {
  10917. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10918. }
  10919. };
  10920. checkInitialized();
  10921. });
  10922. }
  10923.  
  10924. /**
  10925. * 与languageSocket通信的包装方法
  10926. */
  10927. async function sendMessage(data, requiresResponse, callback) {
  10928. if (!initialized) {
  10929. await waitForInitialized(); // 等待initialized为真
  10930. }
  10931. if (requiresResponse) {
  10932. responseHandlers.set(data.id, callback) // 将事件触发函数与id关联起来
  10933. }
  10934. if (!languageSocketState) await waitForLanguageSocketState();
  10935. languageSocket.send(JSON.stringify(data));
  10936. }
  10937. // 发送消息并等待返回结果
  10938. function fetchData(params, callback) {
  10939. sendMessage(params, true, callback);
  10940. }
  10941. // 发送消息,不需要等待返回结果
  10942. function sendData(data) {
  10943. sendMessage(data, false);
  10944. }
  10945.  
  10946. /**
  10947. * 代码文件更新fileWebSocket
  10948. */
  10949. var fileWebSocket = new WebSocket(url + "file");
  10950. var fileWebSocketState = false;
  10951. OJBetter.monaco.lsp.socket.push(fileWebSocket);
  10952. fileWebSocket.onopen = () => {
  10953. fileWebSocketState = true;
  10954. pushLSPLogMessage("info", `fileWebSocket ${i18next.t('lsp.socket.open', { ns: 'logMessage' })}`);
  10955. };
  10956. fileWebSocket.onclose = (ev) => {
  10957. fileWebSocketState = false;
  10958. pushLSPLogMessage("warn", `fileWebSocket ${i18next.t('lsp.socket.close', { ns: 'logMessage' })}`, ev);
  10959. };
  10960. fileWebSocket.onmessage = (ev) => {
  10961. let message = JSON.parse(ev.data);
  10962. if (message.result !== "ok")
  10963. pushLSPLogMessage("error", `update file failed: ${ev}`);
  10964. };
  10965. fileWebSocket.onerror = (error) => {
  10966. console.warn(`Error connecting to fileWebSocket: ${error}`);
  10967. };
  10968. async function updateFile(workspace, filename, fileExtension, code) {
  10969. async function waitForfileWebSocketState() {
  10970. return new Promise((resolve) => {
  10971. const checkInitialized = () => {
  10972. if (fileWebSocketState) {
  10973. resolve();
  10974. } else {
  10975. setTimeout(checkInitialized, 100); // 每100毫秒检查一次initialized的值
  10976. }
  10977. };
  10978. checkInitialized();
  10979. });
  10980. }
  10981. if (!fileWebSocketState) await waitForfileWebSocketState();
  10982. fileWebSocket.send(
  10983. JSON.stringify({
  10984. type: "update",
  10985. workspace,
  10986. filename,
  10987. fileExtension,
  10988. code,
  10989. })
  10990. );
  10991. }
  10992.  
  10993. /**
  10994. * 发送初始化请求
  10995. */
  10996. OJBetter_monaco.Initialize = () => {
  10997. //初始化initialize
  10998. const capabilities = {
  10999. workspace: {
  11000. applyEdit: true,
  11001. },
  11002. textDocument: {
  11003. publishDiagnostics: {
  11004. relatedInformation: true,
  11005. versionSupport: false,
  11006. tagSupport: {
  11007. valueSet: [1, 2],
  11008. },
  11009. codeDescriptionSupport: true,
  11010. },
  11011. completion: {
  11012. contextSupport: true,
  11013. completionItem: {
  11014. snippetSupport: true,
  11015. commitCharactersSupport: true,
  11016. documentationFormat: ["markdown", "plaintext"],
  11017. deprecatedSupport: true,
  11018. preselectSupport: true,
  11019. tagSupport: {
  11020. valueSet: [1],
  11021. },
  11022. insertReplaceSupport: true,
  11023. resolveSupport: {
  11024. properties: [
  11025. "documentation",
  11026. "detail",
  11027. "additionalTextEdits",
  11028. ],
  11029. },
  11030. insertTextModeSupport: {
  11031. valueSet: [1, 2],
  11032. },
  11033. },
  11034. },
  11035. hover: {
  11036. dynamicRegistration: true,
  11037. contentFormat: ["markdown", "plaintext"],
  11038. },
  11039. signatureHelp: {
  11040. signatureInformation: {
  11041. documentationFormat: ["markdown", "plaintext"],
  11042. parameterInformation: {
  11043. labelOffsetSupport: true,
  11044. },
  11045. activeParameterSupport: true,
  11046. },
  11047. contextSupport: true,
  11048. },
  11049. definition: {
  11050. dynamicRegistration: true,
  11051. linkSupport: true,
  11052. },
  11053. references: {
  11054. dynamicRegistration: true,
  11055. },
  11056. documentHighlight: {
  11057. dynamicRegistration: true,
  11058. },
  11059. codeAction: {
  11060. codeActionLiteralSupport: {
  11061. codeActionKind: {
  11062. valueSet:
  11063. language == "java"
  11064. ? []
  11065. : [
  11066. "",
  11067. "quickfix",
  11068. "refactor",
  11069. "refactor.extract",
  11070. "refactor.inline",
  11071. "refactor.rewrite",
  11072. "source",
  11073. "source.organizeImports",
  11074. ],
  11075. },
  11076. },
  11077. },
  11078. rename: {
  11079. dynamicRegistration: true,
  11080. prepareSupport: true,
  11081. prepareSupportDefaultBehavior: 1,
  11082. honorsChangeAnnotations: true,
  11083. },
  11084. documentLink: {
  11085. tooltipSupport: true,
  11086. },
  11087. typeDefinition: {
  11088. dynamicRegistration: true,
  11089. linkSupport: true,
  11090. },
  11091. implementation: {
  11092. dynamicRegistration: true,
  11093. linkSupport: true,
  11094. },
  11095. colorProvider: {
  11096. dynamicRegistration: true,
  11097. },
  11098. foldingRange: {
  11099. dynamicRegistration: true,
  11100. rangeLimit: 5000,
  11101. lineFoldingOnly: true,
  11102. },
  11103. declaration: {
  11104. dynamicRegistration: true,
  11105. linkSupport: true,
  11106. },
  11107. semanticTokens: {
  11108. dynamicRegistration: true,
  11109. tokenTypes: [
  11110. "namespace",
  11111. "type",
  11112. "class",
  11113. "enum",
  11114. "interface",
  11115. "struct",
  11116. "typeParameter",
  11117. "parameter",
  11118. "variable",
  11119. "property",
  11120. "enumMember",
  11121. "event",
  11122. "function",
  11123. "method",
  11124. "macro",
  11125. "keyword",
  11126. "modifier",
  11127. "comment",
  11128. "string",
  11129. "number",
  11130. "regexp",
  11131. "operator",
  11132. ],
  11133. tokenModifiers: [
  11134. "declaration",
  11135. "definition",
  11136. "readonly",
  11137. "static",
  11138. "deprecated",
  11139. "abstract",
  11140. "async",
  11141. "modification",
  11142. "documentation",
  11143. "defaultLibrary",
  11144. ],
  11145. formats: ["relative"],
  11146. requests: {
  11147. range: true,
  11148. full: {
  11149. delta: true,
  11150. },
  11151. },
  11152. multilineTokenSupport: false,
  11153. overlappingTokenSupport: false,
  11154. },
  11155. callHierarchy: {
  11156. dynamicRegistration: true,
  11157. },
  11158. },
  11159. window: {
  11160. showMessage: {
  11161. messageActionItem: {
  11162. additionalPropertiesSupport: true,
  11163. },
  11164. },
  11165. showDocument: {
  11166. support: true,
  11167. },
  11168. workDoneProgress: true,
  11169. },
  11170. general: {
  11171. regularExpressions: {
  11172. engine: "ECMAScript",
  11173. version: "ES2020",
  11174. },
  11175. markdown: {
  11176. parser: "marked",
  11177. version: "1.1.0",
  11178. },
  11179. },
  11180. };
  11181.  
  11182. const initializeRequest = {
  11183. id: id++,
  11184. jsonrpc: "2.0",
  11185. method: "initialize",
  11186. params: {
  11187. processId: null,
  11188. clientInfo: {
  11189. name: "CFMonaco" + InstanceID,
  11190. },
  11191. locale: "zh-CN",
  11192. rootPath: null,
  11193. rootUri: null,
  11194. capabilities: capabilities,
  11195. trace: "off",
  11196. workspaceFolders: [
  11197. {
  11198. uri:
  11199. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11200. name:
  11201. "file:///" + OJBetter.monaco.lsp.workUri + workspace,
  11202. },
  11203. ],
  11204. },
  11205. };
  11206. languageSocket.send(JSON.stringify(initializeRequest));
  11207.  
  11208. // 打开文档函数
  11209. OJBetter_monaco.openDocRequest = function () {
  11210. const initializ = {
  11211. jsonrpc: "2.0",
  11212. method: "initialized",
  11213. params: {},
  11214. };
  11215. languageSocket.send(JSON.stringify(initializ));
  11216. const openDocRequest = {
  11217. jsonrpc: "2.0",
  11218. method: "textDocument/didOpen",
  11219. params: {
  11220. textDocument: {
  11221. uri: model.uri.toString(),
  11222. languageId: language,
  11223. version: model.getVersionId(),
  11224. text: model.getValue(),
  11225. },
  11226. },
  11227. };
  11228. languageSocket.send(JSON.stringify(openDocRequest));
  11229. initialized = true; // 初始化完成,这里确认逻辑待完善
  11230. };
  11231.  
  11232. // 初始化更新文件
  11233. updateFile(workspace, filename, fileExtension, model.getValue());
  11234. }
  11235.  
  11236. /**
  11237. * 注册语言及功能
  11238. */
  11239. OJBetter_monaco.RegistrationAfterInit = () => {
  11240. // 注册语言
  11241. monaco.languages.register({ id: language });
  11242.  
  11243. // 注册"Command"
  11244. (function registerCommand() {
  11245. serverInfo.capabilities.executeCommandProvider.commands.forEach(
  11246. (item) => {
  11247. pushLSPLogMessage("info", `${i18next.t('lsp.server.regist', { ns: 'logMessage' })}`, item);
  11248. monaco.editor.registerCommand(item, (accessor, ...args) => {
  11249. sendData({
  11250. jsonrpc: "2.0",
  11251. id: id++,
  11252. method: "workspace/executeCommand",
  11253. params: {
  11254. command: item,
  11255. arguments: args,
  11256. },
  11257. });
  11258. });
  11259. }
  11260. );
  11261. })();
  11262.  
  11263. // 注册"增量更新"
  11264. model.onDidChangeContent((event) => {
  11265. updateFile(workspace, filename, fileExtension, model.getValue()); // 更新文件
  11266. const changeDocRequest = {
  11267. jsonrpc: "2.0",
  11268. method: "textDocument/didChange",
  11269. params: {
  11270. textDocument: {
  11271. uri: model.uri.toString(),
  11272. version: model.getVersionId(),
  11273. },
  11274. contentChanges: event.changes.map((change) => ({
  11275. range: OJBetter_monaco.MonacoRangeTolspRange(change.range),
  11276. rangeLength: change.rangeLength,
  11277. text: change.text,
  11278. })),
  11279. },
  11280. };
  11281. sendData(changeDocRequest);
  11282. });
  11283.  
  11284. //注册"自动补全"
  11285. monaco.languages.registerCompletionItemProvider(language, {
  11286. provideCompletionItems: (model, position, context) => {
  11287. const request = {
  11288. jsonrpc: "2.0",
  11289. id: id++,
  11290. method: "textDocument/completion",
  11291. params: {
  11292. textDocument: {
  11293. uri: model.uri.toString(),
  11294. },
  11295. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11296. context: {
  11297. triggerKind: context.triggerKind + 1, // 这里要+1,两边的定义不一样。。。
  11298. triggerCharacter: context.triggerCharacter,
  11299. },
  11300. },
  11301. };
  11302. return new Promise((resolve, reject) => {
  11303. fetchData(request, (response) => {
  11304. const result = response.result;
  11305. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11306. if (!result) return resolve(null);
  11307. const CompletionItems = {
  11308. suggestions: result.items.map(
  11309. ({
  11310. label,
  11311. kind,
  11312. filterText,
  11313. insertText,
  11314. insertTextFormat,
  11315. sortText,
  11316. textEdit,
  11317. documentation,
  11318. additionalTextEdits,
  11319. }) => ({
  11320. additionalTextEdits: additionalTextEdits
  11321. ? additionalTextEdits.map(({ newText, range }) => ({
  11322. text: newText,
  11323. range: OJBetter_monaco.lspRangeToMonacoRange(range),
  11324. }))
  11325. : [],
  11326. documentation: documentation ? documentation.value : "",
  11327. filterText,
  11328. insertText: insertText ? insertText : textEdit.newText,
  11329. insertTextRules:
  11330. insertTextFormat === 2
  11331. ? monaco.languages.CompletionItemInsertTextRule
  11332. .InsertAsSnippet
  11333. : monaco.languages.CompletionItemInsertTextRule
  11334. .KeepWhitespace,
  11335. kind,
  11336. label,
  11337. sortText,
  11338. range: textEdit
  11339. ? textEdit.range
  11340. ? OJBetter_monaco.lspRangeToMonacoRange(textEdit.range)
  11341. : OJBetter_monaco.lspRangeToMonacoRange(textEdit.insert)
  11342. : null,
  11343. })
  11344. ),
  11345. };
  11346. pushLSPLogMessage("info", `completion ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, CompletionItems);
  11347. resolve(CompletionItems);
  11348. });
  11349. });
  11350. },
  11351. });
  11352.  
  11353. // 注册"代码修复"
  11354. monaco.languages.registerCodeActionProvider(language, {
  11355. provideCodeActions: (model, range, context) => {
  11356. const request = {
  11357. id: id++,
  11358. jsonrpc: "2.0",
  11359. method: "textDocument/codeAction",
  11360. params: {
  11361. textDocument: {
  11362. uri: model.uri.toString(),
  11363. },
  11364. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11365. context: {
  11366. diagnostics: context.markers.map((item) => ({
  11367. range: OJBetter_monaco.MonacoRangeTolspRange({
  11368. startLineNumber: item.startLineNumber,
  11369. startColumn: item.startColumn,
  11370. endLineNumber: item.endLineNumber,
  11371. endColumn: item.endColumn,
  11372. }),
  11373. severity: OJBetter_monaco.MonacoSeverityTolspSeverity(
  11374. item.severity
  11375. ),
  11376. code: item.code,
  11377. source: item.source,
  11378. message: item.message,
  11379. tags: item.tags,
  11380. relatedInformation: item.relatedInformation
  11381. ? item.relatedInformation.map((item) => ({
  11382. location: {
  11383. uri: item.resource.toString(),
  11384. range: OJBetter_monaco.MonacoRangeTolspRange({
  11385. startLineNumber: item.startLineNumber,
  11386. startColumn: item.startColumn,
  11387. endLineNumber: item.endLineNumber,
  11388. endColumn: item.endColumn,
  11389. }),
  11390. },
  11391. message: item.message,
  11392. }))
  11393. : null,
  11394. })),
  11395. only: context.only ? [context.only] : [],
  11396. triggerKind: context.trigger,
  11397. },
  11398. },
  11399. };
  11400.  
  11401. return new Promise((resolve, reject) => {
  11402. fetchData(request, (response) => {
  11403. const result = response.result;
  11404. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11405. if (!result) return resolve(null);
  11406. const codeAction = {
  11407. actions: result.map((item) => ({
  11408. title: item.title,
  11409. kind: item.kind ? item.kind : "quickfix",
  11410. command: item.command
  11411. ? item.command.command
  11412. ? {
  11413. id: item.command.command,
  11414. arguments: item.command.arguments,
  11415. title: item.command.title,
  11416. }
  11417. : null
  11418. : null,
  11419. diagnostics: item.diagnostics
  11420. ? item.diagnostics.map((item) => ({
  11421. code: item.code,
  11422. message: item.message,
  11423. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11424. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(
  11425. item.severity
  11426. ),
  11427. source: item.source,
  11428. }))
  11429. : null,
  11430. edit: item.edit
  11431. ? OJBetter_monaco.lspEditToMonacoEdit(item.edit)
  11432. : item.arguments
  11433. ? {
  11434. edits: item.arguments.flatMap(
  11435. (item1) => OJBetter_monaco.lspEditToMonacoEdit(item1).edits
  11436. ),
  11437. }
  11438. : null,
  11439. })),
  11440. dispose: () => { },
  11441. };
  11442. pushLSPLogMessage("info", `codeAction ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, codeAction);
  11443.  
  11444. resolve(codeAction);
  11445. });
  11446. });
  11447. },
  11448. });
  11449.  
  11450. // 注册"hover提示"
  11451. monaco.languages.registerHoverProvider(language, {
  11452. provideHover: (model, position) => {
  11453. const request = {
  11454. jsonrpc: "2.0",
  11455. id: id++,
  11456. method: "textDocument/hover",
  11457. params: {
  11458. textDocument: {
  11459. uri: model.uri.toString(),
  11460. },
  11461. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11462. },
  11463. };
  11464.  
  11465. return new Promise((resolve, reject) => {
  11466. fetchData(request, (response) => {
  11467. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11468. const result = response.result;
  11469.  
  11470. if (!result) return resolve(null);
  11471. const Hover = {
  11472. range: result.range
  11473. ? OJBetter_monaco.lspRangeToMonacoRange(result.range)
  11474. : new monaco.Range(
  11475. position.lineNumber,
  11476. position.column,
  11477. position.lineNumber,
  11478. position.column
  11479. ),
  11480. contents: Array.isArray(result.contents)
  11481. ? result.contents.map((item) => ({
  11482. value: item.value ? item.value : item,
  11483. }))
  11484. : [
  11485. {
  11486. value: result.contents.value,
  11487. },
  11488. ],
  11489. };
  11490. pushLSPLogMessage("info", `Hover ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, Hover);
  11491. resolve(Hover);
  11492. });
  11493. });
  11494. },
  11495. });
  11496.  
  11497. // 注册"inlay提示"
  11498. if (language == "cpp" || language == "java")
  11499. monaco.languages.registerInlayHintsProvider(language, {
  11500. provideInlayHints: (model, range, token) => {
  11501. return new Promise((resolve, reject) => {
  11502. const request = {
  11503. jsonrpc: "2.0",
  11504. id: id++,
  11505. method: "textDocument/inlayHint",
  11506. params: {
  11507. textDocument: {
  11508. uri: model.uri.toString(),
  11509. },
  11510. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11511. },
  11512. };
  11513.  
  11514. fetchData(request, (response) => {
  11515. const result = response.result;
  11516. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11517.  
  11518. if (!result) return resolve(null);
  11519.  
  11520. const inlayHints = {
  11521. hints: result.map((item) => {
  11522. return {
  11523. kind: item.kind,
  11524. label: item.label,
  11525. paddingLeft: item.paddingLeft,
  11526. paddingRight: item.paddingRight,
  11527. position: {
  11528. lineNumber: item.position.line + 1,
  11529. column: item.position.character + 1,
  11530. },
  11531. };
  11532. }),
  11533. };
  11534. pushLSPLogMessage("info", `Inlay Hints ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, inlayHints);
  11535.  
  11536. resolve(inlayHints);
  11537. });
  11538. });
  11539. },
  11540. });
  11541.  
  11542. // 注册"转到定义"
  11543. monaco.languages.registerDefinitionProvider(language, {
  11544. provideDefinition: (model, position) => {
  11545. const request = {
  11546. jsonrpc: "2.0",
  11547. id: id++,
  11548. method: "textDocument/definition",
  11549. params: {
  11550. textDocument: {
  11551. uri: model.uri.toString(),
  11552. },
  11553. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11554. },
  11555. };
  11556.  
  11557. return new Promise((resolve, reject) => {
  11558. fetchData(request, (response) => {
  11559. const result = response.result;
  11560. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11561.  
  11562. if (result.length == 0) return resolve(null);
  11563. const definition = result.map((item) => ({
  11564. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11565. uri: monaco.Uri.parse(item.uri), //
  11566. }));
  11567. pushLSPLogMessage("info", `definition ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, definition);
  11568.  
  11569. resolve(definition);
  11570. });
  11571. });
  11572.  
  11573. },
  11574. });
  11575.  
  11576. // 注册"转到引用"
  11577. monaco.languages.registerReferenceProvider(language, {
  11578. provideReferences: (model, position, context) => {
  11579. const request = {
  11580. jsonrpc: "2.0",
  11581. id: id++,
  11582. method: "textDocument/references",
  11583. params: {
  11584. context: context,
  11585. textDocument: {
  11586. uri: model.uri.toString(),
  11587. },
  11588. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11589. },
  11590. };
  11591.  
  11592. return new Promise((resolve, reject) => {
  11593. fetchData(request, (response) => {
  11594. const result = response.result;
  11595. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11596.  
  11597. if (result.length == 0) return resolve([]);
  11598.  
  11599. const references = result.map((item) => ({
  11600. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11601. uri: monaco.Uri.parse(item.uri), //
  11602. }));
  11603. pushLSPLogMessage("info", `references ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, references);
  11604. resolve(references);
  11605. });
  11606. });
  11607. },
  11608. });
  11609.  
  11610. // 注册"符号引用点击高亮"
  11611. monaco.languages.registerDocumentHighlightProvider(language, {
  11612. provideDocumentHighlights: (model, position) => {
  11613. const request = {
  11614. jsonrpc: "2.0",
  11615. id: id++,
  11616. method: "textDocument/documentHighlight",
  11617. params: {
  11618. textDocument: {
  11619. uri: model.uri.toString(),
  11620. },
  11621. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11622. },
  11623. };
  11624.  
  11625. return new Promise((resolve, reject) => {
  11626. fetchData(request, (response) => {
  11627. const result = response.result;
  11628. pushLSPLogMessage("info", `documentHighlight ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11629.  
  11630. if (!result || result.length == 0) return resolve([]);
  11631. const highlights = result.map((item) => ({
  11632. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11633. kind: item.kind,
  11634. }));
  11635. pushLSPLogMessage("info",
  11636. `documentHighlight ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11637. highlights
  11638. );
  11639.  
  11640. resolve(highlights);
  11641. });
  11642. });
  11643. },
  11644. });
  11645.  
  11646. // 注册"文件链接"
  11647. if (language == "cpp" || language == "java")
  11648. monaco.languages.registerLinkProvider(language, {
  11649. provideLinks: (model) => {
  11650. const request = {
  11651. jsonrpc: "2.0",
  11652. id: id++,
  11653. method: "textDocument/documentLink",
  11654. params: {
  11655. textDocument: {
  11656. uri: model.uri.toString(),
  11657. },
  11658. },
  11659. };
  11660.  
  11661. return new Promise((resolve, reject) => {
  11662. fetchData(request, (response) => {
  11663. const result = response.result;
  11664. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11665.  
  11666. if (!result) return resolve(null);
  11667. const links = {
  11668. links: result.map((item) => ({
  11669. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11670. url: item.target.toString(),
  11671. tooltip: item.tooltip ? item.tooltip : null,
  11672. })),
  11673. };
  11674. pushLSPLogMessage("info", `DocumentLink ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, links);
  11675. resolve(links);
  11676. });
  11677. });
  11678. },
  11679. });
  11680.  
  11681. // 注册"格式化"
  11682. monaco.languages.registerDocumentFormattingEditProvider(language, {
  11683. provideDocumentFormattingEdits: (model, options, token) => {
  11684. const request = {
  11685. jsonrpc: "2.0",
  11686. id: id++,
  11687. method: "textDocument/formatting",
  11688. params: {
  11689. textDocument: {
  11690. uri: model.uri.toString(),
  11691. },
  11692. options: options,
  11693. },
  11694. };
  11695.  
  11696. return new Promise((resolve, reject) => {
  11697. fetchData(request, (response) => {
  11698. const result = response.result;
  11699. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11700.  
  11701. const TextEdit = result.map((edit) => ({
  11702. range: OJBetter_monaco.lspRangeToMonacoRange(edit.range),
  11703. text: edit.newText,
  11704. }));
  11705. pushLSPLogMessage("info", `formatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, TextEdit);
  11706. resolve(TextEdit);
  11707. });
  11708. });
  11709. },
  11710. });
  11711.  
  11712. // 注册"部分格式化"
  11713. monaco.languages.registerDocumentRangeFormattingEditProvider(language, {
  11714. provideDocumentRangeFormattingEdits: (model, range, options) => {
  11715. const request = {
  11716. jsonrpc: "2.0",
  11717. id: id++,
  11718. method: "textDocument/rangeFormatting",
  11719. params: {
  11720. textDocument: {
  11721. uri: model.uri.toString(),
  11722. },
  11723. range: OJBetter_monaco.MonacoRangeTolspRange(range),
  11724. options,
  11725. },
  11726. };
  11727.  
  11728. return new Promise((resolve, reject) => {
  11729. fetchData(request, (response) => {
  11730. const result = response.result;
  11731. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11732.  
  11733. if (!result || result.length == 0) return resolve([]);
  11734. const edits = result.map((item) => ({
  11735. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11736. text: item.newText,
  11737. }));
  11738. pushLSPLogMessage("info", `rangeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11739. resolve(edits);
  11740. });
  11741. });
  11742. },
  11743. });
  11744.  
  11745. // 注册"重命名"
  11746. monaco.languages.registerRenameProvider(language, {
  11747. provideRenameEdits: (model, position, newName, token) => {
  11748. const request = {
  11749. jsonrpc: "2.0",
  11750. id: id++,
  11751. method: "textDocument/rename",
  11752. params: {
  11753. textDocument: {
  11754. uri: model.uri.toString(),
  11755. },
  11756. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11757. newName: newName,
  11758. },
  11759. };
  11760.  
  11761. return new Promise((resolve, reject) => {
  11762. fetchData(request, (response) => {
  11763. const result = response.result;
  11764. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11765.  
  11766. const rename = OJBetter_monaco.lspEditToMonacoEdit(result);
  11767. pushLSPLogMessage("info", `rename ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, rename);
  11768. resolve(rename);
  11769. });
  11770. });
  11771. },
  11772. });
  11773.  
  11774. // 注册"折叠范围分析"
  11775. monaco.languages.registerFoldingRangeProvider(language, {
  11776. provideFoldingRanges: (model) => {
  11777. const request = {
  11778. jsonrpc: "2.0",
  11779. id: id++,
  11780. method: "textDocument/foldingRange",
  11781. params: {
  11782. textDocument: {
  11783. uri: model.uri.toString(),
  11784. },
  11785. },
  11786. };
  11787.  
  11788. return new Promise((resolve, reject) => {
  11789. fetchData(request, (response) => {
  11790. const result = response.result;
  11791. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11792.  
  11793. if (!result) return resolve([]);
  11794. const foldingRanges = result.map((item) => ({
  11795. start: item.startLine + 1,
  11796. end: item.endLine + 1,
  11797. kind: monaco.languages.FoldingRangeKind.fromValue(item.kind),
  11798. }));
  11799. pushLSPLogMessage("info", `FoldingRange ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, foldingRanges);
  11800. resolve(foldingRanges);
  11801. });
  11802. });
  11803. },
  11804. });
  11805.  
  11806. // 注册"方法签名提示"
  11807. monaco.languages.registerSignatureHelpProvider(language, {
  11808. signatureHelpTriggerCharacters:
  11809. serverInfo.capabilities.signatureHelpProvider.triggerCharacters,
  11810. provideSignatureHelp: (model, position, token, context) => {
  11811. const request = {
  11812. jsonrpc: "2.0",
  11813. id: id++,
  11814. method: "textDocument/signatureHelp",
  11815. params: {
  11816. textDocument: {
  11817. uri: model.uri.toString(),
  11818. },
  11819. position: {
  11820. line: position.lineNumber - 1,
  11821. character: position.column - 1,
  11822. },
  11823. context: {
  11824. triggerKind: context.triggerKind,
  11825. triggerCharacter: context.triggerCharacter,
  11826. isRetrigger: context.isRetrigger,
  11827. activeSignatureHelp: context.activeSignatureHelp,
  11828. },
  11829. },
  11830. };
  11831.  
  11832. return new Promise((resolve, reject) => {
  11833. fetchData(request, (response) => {
  11834. const result = response.result;
  11835.  
  11836. pushLSPLogMessage("info", `signatureHelp ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11837.  
  11838. if (!result) return resolve(null);
  11839. const SignatureHelpResult = {
  11840. value: {
  11841. activeParameter: result.activeParameter,
  11842. activeSignature: result.activeSignature,
  11843. signatures: result.signatures,
  11844. },
  11845. dispose: () => { },
  11846. };
  11847.  
  11848. pushLSPLogMessage("info",
  11849. `signatureHelp ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`,
  11850. SignatureHelpResult
  11851. );
  11852. resolve(SignatureHelpResult);
  11853. });
  11854. });
  11855. },
  11856. });
  11857.  
  11858. // 注册"渐进式自动格式化" 如果server有这个
  11859. if (serverInfo.capabilities.documentOnTypeFormattingProvider)
  11860. monaco.languages.registerOnTypeFormattingEditProvider(language, {
  11861. autoFormatTriggerCharacters: [
  11862. serverInfo.capabilities.documentOnTypeFormattingProvider
  11863. .firstTriggerCharacter,
  11864. ],
  11865. provideOnTypeFormattingEdits: (model, position, ch, options) => {
  11866. const request = {
  11867. jsonrpc: "2.0",
  11868. id: id++,
  11869. method: "textDocument/onTypeFormatting",
  11870. params: {
  11871. textDocument: {
  11872. uri: model.uri.toString(),
  11873. },
  11874. position: OJBetter_monaco.MonacoPositionTolspPosition(position),
  11875. ch,
  11876. options,
  11877. },
  11878. };
  11879.  
  11880. return new Promise((resolve, reject) => {
  11881. fetchData(request, (response) => {
  11882. const result = response.result;
  11883. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, response);
  11884.  
  11885. if (!result || result.length == 0) return resolve([]);
  11886.  
  11887. const edits = result.map((item) => ({
  11888. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11889. text: item.newText,
  11890. }));
  11891. pushLSPLogMessage("info", `onTypeFormatting ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, edits);
  11892. resolve(edits);
  11893. });
  11894. });
  11895. },
  11896. });
  11897. };
  11898.  
  11899. /**
  11900. * 被动式接收处理
  11901. */
  11902. OJBetter_monaco.PassiveReceiveHandler = () => {
  11903.  
  11904. // "实时代码诊断"
  11905. OJBetter_monaco.updateMarkers = function (message) {
  11906. const params = message.params;
  11907. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11908.  
  11909. if (!params) return;
  11910. const markers = params.diagnostics.map((item1) => ({
  11911. code: item1.code,
  11912. message: item1.message,
  11913. ...OJBetter_monaco.lspRangeToMonacoRange(item1.range),
  11914. relatedInformation: item1.relatedInformation
  11915. ? item1.relatedInformation.map((item2) => ({
  11916. ...(item2.location.range
  11917. ? OJBetter_monaco.lspRangeToMonacoRange(item2.location.range)
  11918. : OJBetter_monaco.lspRangeToMonacoRange(item2.location)),
  11919. message: item2.message,
  11920. resource: monaco.Uri.parse(item2.location.uri),
  11921. }))
  11922. : null,
  11923. severity: OJBetter_monaco.lspSeverityToMonacoSeverity(item1.severity),
  11924. source: item1.source,
  11925. }));
  11926.  
  11927. pushLSPLogMessage("info", `Markers ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, markers);
  11928. monaco.editor.setModelMarkers(model, "eslint", markers);
  11929.  
  11930. // 更新状态底栏信息
  11931. const nowMarks = monaco.editor.getModelMarkers();
  11932. warningCount = 0;
  11933. errorCount = 0;
  11934. for (const marker of nowMarks) {
  11935. if (marker.severity === monaco.MarkerSeverity.Warning) {
  11936. warningCount++;
  11937. } else if (marker.severity === monaco.MarkerSeverity.Error) {
  11938. errorCount++;
  11939. }
  11940. }
  11941. $('#OJBetter_statusBar').text(`Warnings: ${warningCount}, Errors: ${errorCount}`);
  11942. };
  11943.  
  11944. // "应用服务器推送的更改"(代码修复)
  11945. OJBetter_monaco.applyEdit = function (message) {
  11946. const params = message.params;
  11947. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.receive', { ns: 'logMessage' })}`, message);
  11948.  
  11949. if (!params) return;
  11950. const operations = Object.values(params.edit.changes)
  11951. .flat()
  11952. .map((item) => ({
  11953. range: OJBetter_monaco.lspRangeToMonacoRange(item.range),
  11954. text: item.newText,
  11955. }));
  11956.  
  11957. pushLSPLogMessage("info", `applyEdit ${i18next.t('lsp.server.transmit', { ns: 'logMessage' })}`, operations);
  11958. model.pushEditOperations([], operations, () => null); // 入栈编辑操作
  11959. };
  11960. }
  11961.  
  11962. if (!languageSocketState) await waitForLanguageSocketState();
  11963. OJBetter_monaco.Initialize();
  11964. }
  11965.  
  11966. // 语言更改
  11967. function changeMonacoLanguage(form) {
  11968. let nowSelect = form.selectLang.val();
  11969. // 记忆更改
  11970. GM_setValue('compilerSelection', nowSelect);
  11971. // 销毁旧的编辑器
  11972. try {
  11973. if (OJBetter.monaco.editor) OJBetter.monaco.editor.dispose();
  11974. } catch (error) {
  11975. console.warn("Encountered an error while attempting to delete the old editor, but it should not affect your regular usage.", error)
  11976. }
  11977. // 关闭旧的socket
  11978. OJBetter.monaco.lsp.socket.forEach(socket => {
  11979. socket.close();
  11980. });
  11981. // 移除相关元素
  11982. form.topRightDiv.empty();
  11983. $('#LSPLog').remove();
  11984. $('#OJBetter_statusBar').remove();
  11985. // 创建新的编辑器
  11986. if (nowSelect in value_monacoLanguageMap) {
  11987. let language = value_monacoLanguageMap[nowSelect];
  11988. if (language == "python" || language == "cpp") {
  11989. createMonacoEditor(language, form, true);
  11990. } else {
  11991. createMonacoEditor(language, form, false);
  11992. }
  11993. } else {
  11994. createMonacoEditor(null, form, false);
  11995. }
  11996. // 更新在线编译器参数
  11997. changeCompilerArgs(nowSelect);
  11998. }
  11999.  
  12000. // 收集样例数据
  12001. function getTestData() {
  12002. let testData = {};
  12003.  
  12004. /**
  12005. * 从pre中获取文本信息
  12006. * @param {JQuery<HTMLElement>} node 元素
  12007. * @returns {string} 文本信息
  12008. */
  12009. function getTextFromPre(node) {
  12010. let text;
  12011. if (node.find("br").length > 0) {
  12012. text = node.html().replace(/<br>/g, "\n"); // <br>作换行符的情况
  12013. } else {
  12014. text = node.text();
  12015. }
  12016. return text;
  12017. }
  12018.  
  12019. $('.input').each(function (index) {
  12020. var inputText = '';
  12021. if ($(this).find('pre').find('div').length > 0) {
  12022. $(this).find('pre').find('div').each(function () {
  12023. inputText += getTextFromPre($(this)) + '\n';
  12024. });
  12025. } else {
  12026. inputText = getTextFromPre($(this).find('pre'));
  12027. }
  12028. var outputText = '';
  12029. if ($('.output').eq(index).find('pre').find('div').length > 0) {
  12030. $('.output').eq(index).find('pre').find('div').each(function () {
  12031. inputText += getTextFromPre($(this)) + '\n';
  12032. });
  12033. } else {
  12034. outputText = getTextFromPre($('.output').eq(index).find('pre'));
  12035. }
  12036.  
  12037. testData[index + 1] = {
  12038. input: inputText.trim(),
  12039. output: outputText.trim()
  12040. };
  12041. });
  12042. return testData;
  12043. }
  12044.  
  12045. // 初始化自定义测试数据面板
  12046. function CustomTestInit() {
  12047. const url = window.location.href;
  12048.  
  12049. restoreText();
  12050.  
  12051. // 添加
  12052. $('#addCustomTest').click(function () {
  12053. var sampleDiv = $('<div class="sampleDiv">');
  12054. var inputTextarea = $('<p style="padding: 0px 5px;">input</p><textarea class="dynamicTextarea inputTextarea"></textarea>');
  12055. var outputTextarea = $('<p style="padding: 0px 5px;">output</p><textarea class="dynamicTextarea outputTextarea"></textarea>');
  12056. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  12057. sampleDiv.append(deleteCustomTest);
  12058. sampleDiv.append(inputTextarea);
  12059. sampleDiv.append(outputTextarea);
  12060. $('#customTests').append(sampleDiv);
  12061. });
  12062.  
  12063. // 实时保存文本内容到 IndexedDB 中
  12064. $(document).on('input', '.inputTextarea, .outputTextarea', function () {
  12065. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  12066. var objectStore = OJBetter.common.database.samplesData;
  12067. var samples = {
  12068. url: url,
  12069. samples: []
  12070. };
  12071. var index = 0;
  12072. $('.sampleDiv').each(function () {
  12073. var $sampleDiv = $(this);
  12074. var inputTextarea = $sampleDiv.find('.inputTextarea');
  12075. var outputTextarea = $sampleDiv.find('.outputTextarea');
  12076. $sampleDiv.attr('data-index', index);
  12077. inputTextarea.attr('id', 'input' + index);
  12078. outputTextarea.attr('id', 'output' + index);
  12079. var sample = {
  12080. id: index,
  12081. input: inputTextarea.val(),
  12082. output: outputTextarea.val()
  12083. };
  12084. samples.samples.push(sample);
  12085. index++;
  12086. });
  12087. objectStore.put(samples);
  12088. });
  12089. });
  12090.  
  12091. // 删除
  12092. $(document).on('click', '.deleteCustomTest', function () {
  12093. var $sampleDiv = $(this).closest('.sampleDiv');
  12094. OJBetter.common.database.transaction('rw', OJBetter.common.database.samplesData, function () {
  12095. var objectStore = OJBetter.common.database.samplesData;
  12096. var index = parseInt($sampleDiv.attr('data-index'));
  12097. if (!isNaN(index)) {
  12098. objectStore.get(url).then(row => {
  12099. let samples = row.samples;
  12100. samples.splice(index, 1); // 移除第index个元素
  12101. objectStore.put({
  12102. url: url,
  12103. samples: samples
  12104. });
  12105. })
  12106. }
  12107. $sampleDiv.remove();
  12108. });
  12109. });
  12110.  
  12111. // 恢复保存的内容
  12112. function restoreText() {
  12113. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  12114. return OJBetter.common.database.samplesData.get(url);
  12115. }).then(function (data) {
  12116. if (data.samples && data.samples.length > 0) {
  12117. data.samples.forEach(function (item, index) {
  12118. var sampleDiv = $('<div class="sampleDiv">');
  12119. var inputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">input</p><textarea id="input${index}" class="dynamicTextarea inputTextarea"></textarea>`);
  12120. var outputTextarea = OJB_safeCreateJQElement(`<p style="padding: 0px 5px;">output</p><textarea id="output${index}" class="dynamicTextarea outputTextarea"></textarea>`);
  12121. var deleteCustomTest = OJB_safeCreateJQElement(`<button type="button" class="deleteCustomTest">${closeIcon}</button>`);
  12122.  
  12123. inputTextarea.val(item.input);
  12124. outputTextarea.val(item.output);
  12125.  
  12126. sampleDiv.append(deleteCustomTest);
  12127. sampleDiv.append(inputTextarea);
  12128. sampleDiv.append(outputTextarea);
  12129. sampleDiv.attr('data-index', index)
  12130. $('#customTests').append(sampleDiv);
  12131. });
  12132. }
  12133. });
  12134. }
  12135. }
  12136.  
  12137. // 获取自定义测试数据
  12138. function getCustomTestData() {
  12139. const url = window.location.href;
  12140.  
  12141. return new Promise(function (resolve) {
  12142. var customTestData = {};
  12143. OJBetter.common.database.transaction('r', OJBetter.common.database.samplesData, function () {
  12144. return OJBetter.common.database.samplesData.get(url);
  12145. }).then(function (data) {
  12146. if (!data) resolve(customTestData);
  12147. if (data.samples && data.samples.length > 0) {
  12148. data.samples.forEach(function (item, index) {
  12149. customTestData[index + 1] = {
  12150. input: item.input,
  12151. output: item.output
  12152. };
  12153. });
  12154. }
  12155. resolve(customTestData);
  12156. });
  12157. });
  12158. }
  12159.  
  12160. // codeforces编译器参数列表
  12161. let officialLanguage = "";
  12162. function officialCompilerArgsChange(nowSelect) {
  12163. officialLanguage = nowSelect;
  12164. $('#CompilerArgsInput').prop("disabled", true);
  12165. }
  12166.  
  12167. // codeforces编译器通信
  12168. async function officialCompiler(code, input) {
  12169. const data = new FormData();
  12170. data.append('csrf_token', OJBetter.common.cf_csrf_token);
  12171. data.append('source', code);
  12172. data.append('tabSize', '4');
  12173. data.append('programTypeId', officialLanguage);
  12174. data.append('input', input);
  12175. data.append('output', '');
  12176. data.append('communityCode', '');
  12177. data.append('action', 'submitSourceCode');
  12178. data.append('programTypeId', officialLanguage);
  12179. data.append('sourceCode', code);
  12180.  
  12181. const requestOptions = {
  12182. method: 'POST',
  12183. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12184. data: data,
  12185. headers: {
  12186. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12187. }
  12188. };
  12189.  
  12190. const result = {
  12191. Errors: '',
  12192. Result: '',
  12193. Stats: ''
  12194. };
  12195.  
  12196. try {
  12197. const submitResponse = await OJB_GMRequest(requestOptions);
  12198. if (submitResponse.status !== 200 || !submitResponse.response) {
  12199. result.Errors = `${i18next.t('compiler.official.pushError', { ns: 'codeEditor' })}`;
  12200. return result;
  12201. }
  12202.  
  12203. const submitResult = JSON.parse(submitResponse.response);
  12204. const customTestSubmitId = submitResult.customTestSubmitId;
  12205.  
  12206. const verdictResponse = await OJB_promiseRetryWrapper(
  12207. getOfficialCompilerVerdict,
  12208. {
  12209. maxRetries: 10,
  12210. retryInterval: 500
  12211. },
  12212. customTestSubmitId
  12213. );
  12214. return verdictResponse;
  12215. } catch (error) {
  12216. result.Errors = error.message;
  12217. return result;
  12218. }
  12219. }
  12220.  
  12221. // 获取codeforces编译器的执行结果
  12222. async function getOfficialCompilerVerdict(customTestSubmitId) {
  12223. const newdata = new FormData();
  12224. newdata.append('csrf_token', OJBetter.common.cf_csrf_token);
  12225. newdata.append('action', 'getVerdict');
  12226. newdata.append('customTestSubmitId', customTestSubmitId);
  12227.  
  12228. const requestOptions = {
  12229. method: 'POST',
  12230. url: `${OJBetter.common.hostAddress}/data/customtest`,
  12231. data: newdata,
  12232. headers: {
  12233. 'X-Csrf-Token': OJBetter.common.cf_csrf_token
  12234. }
  12235. };
  12236.  
  12237. const responseDetails = await OJB_GMRequest(requestOptions);
  12238. if (responseDetails.status !== 200 || !responseDetails.response) {
  12239. throw new Error(`${i18next.t('compiler.official.getResultError', { ns: 'codeEditor' })}`);
  12240. }
  12241.  
  12242. const response = JSON.parse(responseDetails.response);
  12243. if (!response.stat) {
  12244. throw new Error('Verdict not ready, retrying...');
  12245. }
  12246.  
  12247. return {
  12248. Errors: response.verdict === "OK" ? null : response.verdict + '<br>' + response.output,
  12249. Result: response.output.replace(/\r\n/g, "\n"),
  12250. Stats: `Status: ${response.stat}`
  12251. };
  12252. }
  12253.  
  12254. // rextester编译器参数列表
  12255. let rextesterLanguage = "";
  12256. function rextesterCompilerArgsChange(nowSelect) {
  12257. let LanguageChoiceList = {
  12258. "4": "9", "6": "8", "7": "5", "9": "1", "13": "13", "19": "42", "20": "21", "28": "30", "31": "24", "32": "20",
  12259. "34": "17", "36": "4", "43": "6", "45": "7", "46": "4", "50": "7", "51": "9", "52": "27", "54": "7", "55": "23", "60": "4",
  12260. "61": "7", "65": "1", "67": "12", "70": "5", "73": "7", "74": "4", "75": "46", "77": "43", "79": "1", "80": "27", "83": "43", "87": "4"
  12261. }
  12262. let CompilerArgsList = {
  12263. "6": "-Wall -std=gnu99 -O2 -o a.out source_file.c",
  12264. "7": "-Wall -std=c++14 -O2 -o a.out source_file.cpp",
  12265. "20": "-o a.out source_file.go",
  12266. "27": "-Wall -std=c++14 -stdlib=libc++ -O2 -o a.out source_file.cpp",
  12267. "30": "source_file.d -ofa.out"
  12268. }
  12269. if (nowSelect in LanguageChoiceList) {
  12270. $('#RunTestButton').prop("disabled", false);
  12271. rextesterLanguage = LanguageChoiceList[nowSelect];
  12272. } else {
  12273. $('#RunTestButton').prop("disabled", true);
  12274. }
  12275. if (rextesterLanguage in CompilerArgsList) {
  12276. const rextesterCompilerArgs = CompilerArgsList[rextesterLanguage];
  12277. $('#CompilerArgsInput').val(rextesterCompilerArgs);
  12278. } else {
  12279. $('#CompilerArgsInput').val("");
  12280. }
  12281. }
  12282.  
  12283. // rextester编译器通信
  12284. async function rextesterCompiler(code, input) {
  12285. try {
  12286. const result = await OJB_promiseRetryWrapper(rextesterCompilerRequest, {
  12287. maxRetries: 5,
  12288. retryInterval: 500,
  12289. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12290. }, code, input);
  12291. return result;
  12292. } catch (error) {
  12293. return { Errors: error.message, Result: '', Stats: '' };
  12294. }
  12295. }
  12296.  
  12297. // rextester编译器请求方法
  12298. async function rextesterCompilerRequest(code, input) {
  12299. const data = new FormData();
  12300. data.append('LanguageChoiceWrapper', rextesterLanguage);
  12301. data.append('EditorChoiceWrapper', '1');
  12302. data.append('LayoutChoiceWrapper', '1');
  12303. data.append('Program', code);
  12304. data.append('CompilerArgs', $('#CompilerArgsInput').val());
  12305. data.append('Input', input);
  12306. data.append('ShowWarnings', 'false');
  12307. data.append('IsInEditMode', 'false');
  12308. data.append('IsLive', 'false');
  12309.  
  12310. const responseDetails = await OJB_GMRequest({
  12311. method: 'POST',
  12312. url: 'https://rextester.com/rundotnet/Run',
  12313. data: data
  12314. });
  12315.  
  12316. if (responseDetails.status !== 200 || !responseDetails.response) {
  12317. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12318. }
  12319.  
  12320. const response = JSON.parse(responseDetails.response);
  12321. return {
  12322. Errors: response.Errors || '',
  12323. Result: response.Result || '',
  12324. Stats: response.Stats || ''
  12325. };
  12326. }
  12327.  
  12328. // wandbox编译器参数列表
  12329. var wandboxlist = JSON.parse(GM_getResourceText("wandboxlist"));
  12330. function wandboxCompilerArgsChange(nowSelect) {
  12331. let LanguageChoiceList = {
  12332. "6": "PHP", "7": "Python", "9": "C#", "12": "Haskell", "13": "Perl", "19": "OCaml",
  12333. "20": "Scala", "28": "D", "31": "Python", "32": "Go", "34": "JavaScript", "36": "Java", "40": "Python", "41": "Python",
  12334. "43": "C++", "50": "C++", "51": "Pascal", "52": "C++", "54": "C++", "60": "Java", "61": "C++", "65": "C#", "67": "Ruby",
  12335. "70": "Python", "73": "C++", "74": "Java", "75": "Rust", "79": "C#", "80": "C++", "87": "Java"
  12336. }
  12337.  
  12338. // 移除旧的
  12339. $('#CompilerChange').remove();
  12340.  
  12341. if (nowSelect in LanguageChoiceList) {
  12342. $('#RunTestButton').prop("disabled", false);
  12343. const Languagefiltered = wandboxlist.filter(obj => obj.language === LanguageChoiceList[nowSelect]);
  12344.  
  12345. // 创建编译器下拉框
  12346. let CompilerChange = $('<select id="CompilerChange" style="width: 100%;"></select>');
  12347.  
  12348. $('#CompilerSetting').show().append(CompilerChange);
  12349. for (let i = 0; i < Languagefiltered.length; i++) {
  12350. let Compiler = Languagefiltered[i];
  12351. let op = $("<option></option>")
  12352. .val(Compiler.name)
  12353. .text(Compiler["display-name"] + " " + Compiler.version);
  12354. $("#CompilerChange").append(op);
  12355. }
  12356.  
  12357. // 编译器参数刷新
  12358. function refreshCompilerArgs() {
  12359. var flags = '';
  12360. $("#CompilerBox").find("*").each(function () {
  12361. if ($(this).is("input[type='checkbox']")) {
  12362. let flag = $(this).prop("checked") ? $(this).val() : '';
  12363. flags += flag + (flag ? ' ' : '');
  12364. } else if ($(this).is("select") || $(this).is("input") || $(this).is("textarea")) {
  12365. let flag = $(this).val();
  12366. flags += flag + (flag ? ' ' : '');
  12367. }
  12368. });
  12369. $("#CompilerArgsInput").val(flags);
  12370. $("#CompilerArgsInput").prop("readonly", true); // 只读
  12371. }
  12372.  
  12373. // 编译器切换监听
  12374. CompilerChange.change(function () {
  12375. let selectedName = CompilerChange.val();
  12376. let Compiler = Languagefiltered.find(
  12377. (obj) => obj.name === selectedName
  12378. );
  12379.  
  12380. $("#CompilerArgsInput").val(); // 初始化编译器输入框
  12381.  
  12382. $("#CompilerBox").remove();
  12383. let div = $("<div id='CompilerBox'></div>");
  12384.  
  12385. let display_compile_command = OJB_safeCreateJQElement(`<input id='${Compiler.name}' value='${Compiler['display-compile-command']}' style="display:none;"}></input>`);
  12386. div.append(display_compile_command);
  12387.  
  12388. let switches = Compiler.switches;
  12389. for (let i = 0; i < switches.length; i++) {
  12390. let switche = switches[i];
  12391.  
  12392. if (switche.type == "single") {
  12393. let single = OJB_safeCreateJQElement(`
  12394. <div>
  12395. <input type='checkbox' id='${switche.name}' value='${switche['display-flags']}' ${switche.default ? 'checked' : ''}></input>
  12396. <label for='${switche.name}'>${switche['display-name']}</label>
  12397. </div>
  12398. `);
  12399. div.append(single);
  12400. single.find("input").change(function () {
  12401. refreshCompilerArgs();
  12402. });
  12403. } else if (switche.type == "select") {
  12404. let select = OJB_safeCreateJQElement(`<select id='${switche.name}'></select>`);
  12405. select.data('previousValue', switche.options[0]['display-flags']);
  12406. div.append(select);
  12407. for (let i = 0; i < switche.options.length; i++) {
  12408. let option = switche.options[i];
  12409. let op = $("<option></option>")
  12410. .val(option['display-flags'])
  12411. .text(option['display-name']);
  12412. select.append(op);
  12413. }
  12414. select.change(function () {
  12415. refreshCompilerArgs();
  12416. });
  12417. }
  12418. }
  12419.  
  12420. if (Compiler['compiler-option-raw'] == true) {
  12421. let textarea = OJB_safeCreateJQElement(`<textarea id="compiler_option_raw" placeholder="Raw compiler options" style="resize: vertical;"></textarea>`);
  12422. div.append(textarea);
  12423. textarea.on('input', function () {
  12424. refreshCompilerArgs();
  12425. });
  12426. }
  12427. if (Compiler['runtime-option-raw'] == true) {
  12428. let textarea = OJB_safeCreateJQElement(`<textarea id="runtime_option_raw" placeholder="Raw runtime options" style="resize: vertical;"></textarea>`);
  12429. div.append(textarea);
  12430. textarea.on('input', function () {
  12431. refreshCompilerArgs();
  12432. });
  12433. }
  12434. $("#CompilerSetting").append(div);
  12435.  
  12436. refreshCompilerArgs(); // 初始化
  12437. });
  12438.  
  12439. CompilerChange.trigger("change"); // 初始化
  12440. } else {
  12441. $('#RunTestButton').prop("disabled", true);
  12442. }
  12443. }
  12444.  
  12445. // wandbox编译器通信
  12446. async function wandboxCompiler(code, input) {
  12447. try {
  12448. const result = await OJB_promiseRetryWrapper(wandboxCompilerRequest, {
  12449. maxRetries: 5,
  12450. retryInterval: 500,
  12451. errorHandler: (err) => ({ Errors: err.message, Result: '', Stats: '' })
  12452. }, code, input);
  12453. return result;
  12454. } catch (error) {
  12455. return { Errors: error.message, Result: '', Stats: '' };
  12456. }
  12457. }
  12458.  
  12459. // wandbox编译器请求方法
  12460. async function wandboxCompilerRequest(code, input) {
  12461. const data = {
  12462. code: code,
  12463. codes: [],
  12464. compiler: $('#CompilerChange').val().replace($('#compiler_option_raw').val(), '').replace($('#runtime_option_raw').val(), ''),
  12465. 'compiler-option-raw': $('#compiler_option_raw').val(),
  12466. 'runtime-option-raw': $('#runtime_option_raw').val(),
  12467. options: $("#CompilerArgsInput").val(),
  12468. description: '',
  12469. stdin: input,
  12470. title: ''
  12471. };
  12472.  
  12473. const responseDetails = await OJB_GMRequest({
  12474. method: 'POST',
  12475. url: 'https://wandbox.org/api/compile.json',
  12476. data: JSON.stringify(data),
  12477. headers: {
  12478. 'Content-Type': 'application/json'
  12479. }
  12480. });
  12481.  
  12482. if (responseDetails.status !== 200 || !responseDetails.response) {
  12483. throw new Error(`${i18next.t('compiler.common.error', { ns: 'codeEditor' })}`);
  12484. }
  12485.  
  12486. const response = JSON.parse(responseDetails.response);
  12487. return {
  12488. Errors: response.compiler_error === "" ? response.signal : response.compiler_error,
  12489. Result: response.program_output || '',
  12490. Stats: response.status === "0" ? "Finish" : "Error"
  12491. };
  12492. }
  12493.  
  12494. // 更改编译器参数
  12495. function changeCompilerArgs(nowSelect) {
  12496. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12497. officialCompilerArgsChange(nowSelect);
  12498. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12499. rextesterCompilerArgsChange(nowSelect);
  12500. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12501. wandboxCompilerArgsChange(nowSelect);
  12502. }
  12503. }
  12504.  
  12505. // 在线编译器通信
  12506. async function onlineCompilerConnect(code, input) {
  12507. if (OJBetter.monaco.onlineCompilerChoice == "official") {
  12508. return await officialCompiler(code, input);
  12509. } else if (OJBetter.monaco.onlineCompilerChoice == "rextester") {
  12510. return await rextesterCompiler(code, input);
  12511. } else if (OJBetter.monaco.onlineCompilerChoice == "wandbox") {
  12512. return await wandboxCompiler(code, input);
  12513. }
  12514. }
  12515.  
  12516. // 差异对比
  12517. function codeDiff(expectedText, actualText) {
  12518. // 将文本按行拆分
  12519. const expectedLines = expectedText ? expectedText.split('\n') : [];
  12520. const actualLines = actualText ? actualText.split('\n') : [];
  12521.  
  12522. const output = document.createElement('div');
  12523.  
  12524. const createLineElement = (lineNo, contentElement) => {
  12525. const lineDiv = document.createElement('div');
  12526. lineDiv.className = 'diffLine';
  12527.  
  12528. const lineNoDiv = document.createElement('div');
  12529. lineNoDiv.className = 'lineNo';
  12530. lineNoDiv.textContent = lineNo;
  12531.  
  12532. lineDiv.appendChild(lineNoDiv);
  12533. lineDiv.appendChild(contentElement);
  12534.  
  12535. return lineDiv;
  12536. };
  12537.  
  12538. const createContentElement = (isEquals = true, expected = null, removed = null) => {
  12539. const contentDiv = document.createElement('div');
  12540. contentDiv.className = 'lineContent';
  12541.  
  12542. if (isEquals) {
  12543. const span = document.createElement('span');
  12544. span.textContent = expected;
  12545. contentDiv.appendChild(span);
  12546. } else {
  12547. if (removed != null) {
  12548. const removedSpan = document.createElement('span');
  12549. removedSpan.className = 'removed';
  12550. removedSpan.textContent = removed;
  12551. contentDiv.appendChild(removedSpan);
  12552. }
  12553. if (expected != null) {
  12554. const addedSpan = document.createElement('span');
  12555. addedSpan.className = 'added';
  12556. addedSpan.textContent = expected;
  12557. contentDiv.appendChild(addedSpan);
  12558. }
  12559. }
  12560.  
  12561. return contentDiv;
  12562. };
  12563.  
  12564. let index = 1;
  12565.  
  12566. expectedLines.forEach((expectedLine, i) => {
  12567. const actualLine = actualLines[i];
  12568. if (actualLine === undefined) {
  12569. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine)));
  12570. } else if (expectedLine === actualLine) {
  12571. output.appendChild(createLineElement(index++, createContentElement(true, expectedLine)));
  12572. } else {
  12573. output.appendChild(createLineElement(index++, createContentElement(false, expectedLine, actualLine)));
  12574. }
  12575. });
  12576.  
  12577. // 处理多余的 actualLines
  12578. for (let i = expectedLines.length; i < actualLines.length; i++) {
  12579. output.appendChild(createLineElement(index++, createContentElement(false, null, actualLines[i])));
  12580. }
  12581.  
  12582. return output.innerHTML;
  12583. }
  12584.  
  12585. // 内容类型常量
  12586. const TestCaseContentType = {
  12587. TERMINAL: 'terminal',
  12588. DIFF: 'diff',
  12589. NO_DIFF: 'no_diff',
  12590. SUCCESS: 'success'
  12591. };
  12592.  
  12593. // 样例测试状态类
  12594. class TestCaseStatus {
  12595. constructor(item, prefix) {
  12596. this.testCase = OJB_safeCreateJQElement(`<div class="test-case"></div>`);
  12597. this.item = item;
  12598. this.prefix = prefix;
  12599. this.titleElement = OJB_safeCreateJQElement(`<div class="test-case-title">${this.prefix} ${this.item}</div>`);
  12600. this.statusElement = OJB_safeCreateJQElement(`<div class="test-case-status"></div>`);
  12601. this.contentElement = OJB_safeCreateJQElement(`<div class="test-case-content"></div>`);
  12602. this.judgeElement = OJB_safeCreateJQElement(`<div class="test-case-judge"></div>`);
  12603. this.testCase.append(this.titleElement, this.statusElement, this.contentElement, this.judgeElement);
  12604. $('#statePanel').append(this.testCase);
  12605. this.setStatus('Queued', 'queued');
  12606. }
  12607.  
  12608. setTitle(title) {
  12609. this.titleElement.text(title);
  12610. }
  12611.  
  12612. setStatus(text, status) {
  12613. this.statusElement.text(text).removeClass('queued running success error').addClass(status);
  12614. }
  12615.  
  12616. setContent(content, type) {
  12617. // 如果内容类型为SUCCESS,隐藏内容元素并提前返回
  12618. if (type === TestCaseContentType.SUCCESS) {
  12619. this.contentElement.hide();
  12620. return;
  12621. }
  12622.  
  12623. // 根据内容类型创建内容元素
  12624. const createContentElementByType = (content, type) => {
  12625. let contentElement;
  12626. switch (type) {
  12627. case TestCaseContentType.TERMINAL:
  12628. // 为TERMINAL类型创建一个新的终端容器
  12629. contentElement = OJB_safeCreateJQElement(`<div class="terminal-container" style="overflow: auto;"></div>`);
  12630. break;
  12631. case TestCaseContentType.DIFF:
  12632. case TestCaseContentType.NO_DIFF:
  12633. // 为DIFF和NO_DIFF类型创建相应的内容元素,并添加差异说明
  12634. const className = type === TestCaseContentType.DIFF ? "output_diff" : "output_no_diff";
  12635. contentElement = OJB_safeCreateJQElement(`<pre class="${className}">${content}</pre>`);
  12636. appendDiffNote();
  12637. break;
  12638. default:
  12639. throw new Error("Unsupported content type.");
  12640. }
  12641. return contentElement;
  12642. };
  12643.  
  12644. // 初始化终端
  12645. const initializeTerminal = (content, contentElement) => {
  12646. const term = new Terminal({ rows: 10, cols: 150 });
  12647. term.setOption('theme', { background: '#2d2e2c' });
  12648. term.setOption('convertEol', true); // 将换行符\n转换为\r\n
  12649. term.write(content);
  12650. term.open(contentElement.get(0));
  12651. };
  12652.  
  12653. // 添加差异说明
  12654. const appendDiffNote = () => {
  12655. const diffNote = OJB_safeCreateJQElement(`<div class="diff_note">${i18next.t('resultBlock.diffNote', { ns: 'codeEditor' })}</div>`);
  12656. this.testCase.append(diffNote);
  12657. };
  12658.  
  12659. // 创建并追加内容元素
  12660. const contentElement = createContentElementByType(content, type);
  12661. this.contentElement.append(contentElement);
  12662.  
  12663. // 如果内容类型为TERMINAL,初始化并打开终端
  12664. if (type === TestCaseContentType.TERMINAL) {
  12665. initializeTerminal(content, contentElement);
  12666. }
  12667. }
  12668.  
  12669. setJudge(judge) {
  12670. this.judgeElement.text(judge);
  12671. }
  12672. }
  12673.  
  12674. // 样例测试函数
  12675. async function runCode(event, runButton, sourceDiv) {
  12676. event.preventDefault();
  12677. const statePanel = $('#statePanel').show().empty();
  12678. const testData = getTestData();
  12679. const customTestData = await getCustomTestData();
  12680. const totalTests = Object.keys(customTestData).length + Object.keys(testData).length;
  12681.  
  12682. let passedTests = 0;
  12683. let failedTests = 0;
  12684. let hasError = false;
  12685.  
  12686. // 定义一个对象队列,包括创建的样例块实例和对应的样例数据
  12687. const queue = [];
  12688.  
  12689. // 先生成各个样例的块,并显示排队中,将创建的各个对象存到队列中,以便后面更新
  12690. for (const [item, data] of Object.entries(customTestData)) {
  12691. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.custom', { ns: 'codeEditor' }));
  12692. queue.push({ testCase, data });
  12693. }
  12694.  
  12695. if (!$('#onlyCustomTest').prop('checked')) {
  12696. for (const [item, data] of Object.entries(testData)) {
  12697. const testCase = new TestCaseStatus(item, i18next.t('resultBlock.title.sample', { ns: 'codeEditor' }));
  12698. queue.push({ testCase, data });
  12699. }
  12700. }
  12701.  
  12702. // 测试函数
  12703. const runTest = async (testCase, data, index) => {
  12704. runButton.setButtonState('running', `${index}/${totalTests}`);
  12705.  
  12706. testCase.setStatus('Running', 'running');
  12707. const result = await onlineCompilerConnect(sourceDiv.val(), data.input);
  12708.  
  12709. if (result.Errors) {
  12710. testCase.setStatus('Compilation error or Time limit', 'error');
  12711. testCase.setContent(result.Errors, TestCaseContentType.TERMINAL);
  12712. hasError = true;
  12713. } else if (result.Result.trim() === data.output.trim()) {
  12714. testCase.setStatus('Accepted', 'success');
  12715. testCase.setContent('The output is correct.', TestCaseContentType.SUCCESS);
  12716. passedTests++;
  12717. } else {
  12718. testCase.setStatus('Wrong Answer', 'error');
  12719. const diffContent = $('#DontShowDiff').prop('checked') ? result.Result.trim() : codeDiff(data.output.trim(), result.Result.trim());
  12720. const contentType = $('#DontShowDiff').prop('checked') ? TestCaseContentType.NO_DIFF : TestCaseContentType.DIFF;
  12721. testCase.setContent(diffContent, contentType);
  12722. failedTests++;
  12723. }
  12724.  
  12725. const judgeStats = `${i18next.t('resultBlock.state', { ns: 'codeEditor' })}${result.Stats}`;
  12726. testCase.setJudge(judgeStats);
  12727.  
  12728. await OJB_delay(500); // 等待500毫秒
  12729. };
  12730.  
  12731. // 对队列中的对象进行测试
  12732. for (let i = 0; i < queue.length; i++) {
  12733. const { testCase, data } = queue[i];
  12734. await runTest(testCase, data, i + 1);
  12735. }
  12736.  
  12737. // 测试完成后更新按钮状态
  12738. if (hasError) {
  12739. runButton.setButtonState('error', i18next.t('runTestButton.error', { ns: 'codeEditor' }));
  12740. } else if (failedTests > 0) {
  12741. runButton.setButtonState('error', `${passedTests}/${totalTests} ` + i18next.t('runTestButton.partial', { ns: 'codeEditor' }));
  12742. } else {
  12743. runButton.setButtonState('success', i18next.t('runTestButton.success', { ns: 'codeEditor' }));
  12744. if (OJBetter.monaco.setting.autoSubmitAfterPass) {
  12745. $('#OJBetter_SubmitForm').submit(); // 自动提交
  12746. }
  12747. }
  12748. }
  12749.  
  12750. /**
  12751. * 添加题目页代码编辑器
  12752. * @returns
  12753. */
  12754. async function addProblemPageCodeEditor() {
  12755. if (typeof ace === 'undefined') {
  12756. const loadingMessage = new LoadingMessage();
  12757. loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('error.codeEditor.load', { ns: 'alert' })}`, 'error');
  12758. return; // 因为Codeforces设定的是未登录时不能访问提交页,也不会加载ace库
  12759. }
  12760.  
  12761. // 获取提交页链接
  12762. const href = window.location.href;
  12763. let submitUrl;
  12764. if (/\/problemset\//.test(href)) {
  12765. // problemset
  12766. submitUrl = OJBetter.common.hostAddress + '/problemset/submit';
  12767. } else if (/\/gym\//.test(href)) {
  12768. // gym 题目
  12769. submitUrl = OJBetter.common.hostAddress + '/gym/' + ((href) => {
  12770. const regex = /\/gym\/(?<num>[0-9a-zA-Z]*?)\/problem\//;
  12771. const match = href.match(regex);
  12772. return match && match.groups.num;
  12773. })(href) + '/submit';
  12774. } else if (OJBetter.typeOfPage.is_acmsguru) {
  12775. // acmsguru 题目
  12776. submitUrl = href.replace(/\/problemsets[A-Za-z0-9\/#]*/, "/problemsets/acmsguru/submit");
  12777. } else {
  12778. submitUrl = href.replace(/\/problem[A-Za-z0-9\/#]*/, "/submit");
  12779. }
  12780.  
  12781. // 获取提交页HTML
  12782. let cloneHTML = await getSubmitHTML(submitUrl);
  12783.  
  12784. // 创建
  12785. let form = await createCodeEditorForm(submitUrl, cloneHTML);
  12786. let selectLang = form.selectLang;
  12787. let submitButton = form.submitButton;
  12788. let runButton = form.runButton;
  12789.  
  12790. // 初始化
  12791. CustomTestInit(); // 自定义测试数据面板
  12792. selectLang.val(OJBetter.monaco.compilerSelection);
  12793.  
  12794. // 设置语言选择change事件监听器
  12795. selectLang.on('change', () => {
  12796. changeMonacoLanguage(form); // 编辑器语言切换监听
  12797. });
  12798. changeMonacoLanguage(form);
  12799.  
  12800. // 样例测试
  12801. runButton.on('click', (event) => runCode(event, runButton, form.sourceDiv, form.submitDiv))
  12802. .setHoverRedo();
  12803.  
  12804. // 提交
  12805. submitButton.on('click', async function (event) {
  12806. event.preventDefault();
  12807. if (OJBetter.monaco.setting.isCodeSubmitDoubleConfirm) {
  12808. // 获取题目名
  12809. const questionTitle = (() => {
  12810. if (OJBetter.typeOfPage.is_acmsguru) {
  12811. return $('h4').eq(0).text();
  12812. } else {
  12813. return $('.header .title').eq(0).text();
  12814. }
  12815. })();
  12816. const submit = await OJB_createDialog(
  12817. i18next.t('submitCode.title', { ns: 'dialog' }),
  12818. i18next.t('submitCode.content', { ns: 'dialog', questionTitle: questionTitle }),
  12819. [
  12820. i18next.t('submitCode.buttons.0', { ns: 'dialog' }),
  12821. i18next.t('submitCode.buttons.1', { ns: 'dialog' })
  12822. ],
  12823. true
  12824. ); //提交确认
  12825. if (submit) {
  12826. submitButton.after(`<img class="OJBetter_loding" src="//codeforces.org/s/84141/images/ajax-loading-24x24.gif">`);
  12827. $('#OJBetter_SubmitForm').submit();
  12828. } else {
  12829. submitButton.addClass('disabled');
  12830. setTimeout(function () {
  12831. submitButton.removeClass('disabled');
  12832. }, 300);
  12833. }
  12834. } else {
  12835. $('#OJBetter_SubmitForm').submit();
  12836. }
  12837. });
  12838. }
  12839.  
  12840. /**
  12841. * 获取翻译服务目标语言的对应代码
  12842. * @param {string} serverName 服务名称
  12843. * @returns {string} 目标语言,如果没有对应代码则返回中文
  12844. */
  12845. function getTargetLanguage(serverName) {
  12846. let targetLanguage = OJBetter.supportList.translationSupport[serverName][OJBetter.translation.targetLang];
  12847. if (targetLanguage) return targetLanguage;
  12848. else return OJBetter.supportList.translationSupport[serverName]['zh'];
  12849. }
  12850.  
  12851. /**
  12852. * 将文本中Markdown格式的加粗**转换成HTML格式。
  12853. * @param {string} text 文本
  12854. * @returns {string} 替换后的字符串
  12855. */
  12856. function convertBoldMarkdownToHTML(text) {
  12857. return text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  12858. }
  12859.  
  12860. /**
  12861. * 将文本中Markdown格式的链接文本转换成HTML格式。
  12862. * @param {string} text 文本
  12863. * @returns {string} 替换后的字符串
  12864. */
  12865. function convertLinksMarkdownToHTML(text) {
  12866. return text.replace(/(?<!!)\[(.*?)\]\(([^"]*?)("(.*?)")*\)/g, '<a href="$2" title="$4">$1</a>');
  12867. }
  12868.  
  12869. /**
  12870. * 将HTML格式的加粗文本转换回Markdown格式。
  12871. * @param {string} text 文本
  12872. * @returns {string} 替换后的字符串
  12873. */
  12874. function convertBoldHTMLToMarkdown(text) {
  12875. return text.replace(/<strong>(.*?)<\/strong>/g, '**$1**');
  12876. }
  12877.  
  12878. /**
  12879. * 将HTML格式的链接文本转换回Markdown格式。
  12880. * @param {string} html - 包含HTML链接标签<a>的字符串。
  12881. * @returns {string} 转换后的字符串,其中HTML链接标签被替换为Markdown的链接语法。
  12882. */
  12883. function convertLinksHTMLToMarkdown(html) {
  12884. return html.replace(/<a href="([^"]*)"( title="([^"]*)")*>([^<]+)<\/a>/g, '[$4]($1 "$3")');
  12885. }
  12886.  
  12887. /**
  12888. * DeepL翻译
  12889. * @param {string} raw 原文
  12890. * @returns {Promise<TransRawData>} 翻译结果对象
  12891. */
  12892. async function translate_deepl(raw) {
  12893. const id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
  12894. const data = {
  12895. jsonrpc: '2.0',
  12896. method: 'LMT_handle_texts',
  12897. id,
  12898. params: {
  12899. splitting: 'newlines',
  12900. lang: {
  12901. source_lang_user_selected: 'auto',
  12902. target_lang: getTargetLanguage('deepl'),
  12903. },
  12904. texts: [{
  12905. text: raw,
  12906. requestAlternatives: 3
  12907. }],
  12908. timestamp: getTimeStamp(raw.split('i').length - 1)
  12909. }
  12910. }
  12911. let postData = JSON.stringify(data);
  12912. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  12913. postData = postData.replace('"method":"', '"method" : "');
  12914. } else {
  12915. postData = postData.replace('"method":"', '"method": "');
  12916. }
  12917. const options = {
  12918. method: 'POST',
  12919. url: 'https://www2.deepl.com/jsonrpc',
  12920. data: postData,
  12921. headers: {
  12922. 'Content-Type': 'application/json',
  12923. 'Host': 'www2.deepl.com',
  12924. 'Origin': 'https://www.deepl.com',
  12925. 'Referer': 'https://www.deepl.com/',
  12926. },
  12927. anonymous: true,
  12928. nocache: true,
  12929. }
  12930.  
  12931. return await BaseTranslate(options, res => JSON.parse(res)?.result?.texts?.[0]?.text || res, res => {
  12932. const resObj = {
  12933. status: true,
  12934. message: 'ok'
  12935. };
  12936. if (res.includes('"message":"Too many requests"')) {
  12937. resObj.status = false;
  12938. resObj.message = i18next.t('error.deepl429', { ns: 'translator' }); // Too many requests 提示
  12939. return resObj;
  12940. };
  12941. return resObj;
  12942. });
  12943. }
  12944.  
  12945. /**
  12946. * 使用 DeepL Free API 进行翻译
  12947. * @param {string} raw 原文
  12948. * @returns {Promise<TransRawData>} 翻译结果对象
  12949. */
  12950. async function translate_deepl_api_free(raw) {
  12951. const data = JSON.stringify({
  12952. text: [raw],
  12953. target_lang: getTargetLanguage('deepl'),
  12954. split_sentences: '1',
  12955. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12956. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12957. });
  12958.  
  12959. const options = {
  12960. method: "POST",
  12961. url: OJBetter.deepl.config.proxy || "https://api-free.deepl.com/v2/translate",
  12962. headers: {
  12963. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12964. "Content-Type": "application/json",
  12965. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12966. },
  12967. data: data,
  12968. onload: response => response.responseText,
  12969. onerror: error => console.error(error)
  12970. };
  12971.  
  12972. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  12973. }
  12974.  
  12975. /**
  12976. * 使用 DeepL Pro API 进行翻译
  12977. * @param {string} raw 原文
  12978. * @returns {Promise<TransRawData>} 翻译结果对象
  12979. */
  12980. async function translate_deepl_api_pro(raw) {
  12981. const data = JSON.stringify({
  12982. text: [raw],
  12983. target_lang: getTargetLanguage('deepl'),
  12984. split_sentences: '1',
  12985. ...(OJBetter.deepl.enableEmphasisProtection || OJBetter.deepl.enableLinkProtection ? { tag_handling: 'html' } : {}),
  12986. ...Object.assign({}, ...OJBetter.deepl.config.data)
  12987. });
  12988.  
  12989. const options = {
  12990. method: "POST",
  12991. url: OJBetter.deepl.config.proxy || "https://api.deepl.com/v2/translate",
  12992. headers: {
  12993. "Authorization": `DeepL-Auth-Key ${OJBetter.deepl.config.key}`,
  12994. "Content-Type": "application/json",
  12995. ...Object.assign({}, ...OJBetter.deepl.config.header)
  12996. },
  12997. data: data,
  12998. onload: response => response.responseText,
  12999. onerror: error => console.error(error)
  13000. };
  13001.  
  13002. return await BaseTranslate(options, res => JSON.parse(res).translations[0].text);
  13003. }
  13004.  
  13005. /**
  13006. * 使用 DeepLX 进行翻译
  13007. * @param {String} text 原文
  13008. * @returns {Promise<TransRawData>} 翻译结果对象
  13009. */
  13010. async function translate_deeplx(text) {
  13011. const options = {
  13012. method: "POST",
  13013. url: OJBetter.deepl.config.proxy || 'https://api.deeplx.org/translate',
  13014. data: JSON.stringify({
  13015. "text": text,
  13016. "source_lang": "EN",
  13017. "target_lang": getTargetLanguage('deepl'),
  13018. }),
  13019. headers: {
  13020. 'Content-Type': 'application/json',
  13021. ...(OJBetter.deepl.config.key ? { Authorization: `Bearer ${OJBetter.deepl.config.key}` } : {})
  13022. },
  13023. responseType: "json",
  13024. };
  13025.  
  13026. return await BaseTranslate(options, res => {
  13027. const parsedResponse = JSON.parse(res);
  13028. if (parsedResponse.code === 200 && parsedResponse.data) {
  13029. return parsedResponse.data;
  13030. } else {
  13031. throw new Error('Translation failed or invalid response format.');
  13032. }
  13033. });
  13034. }
  13035.  
  13036. function getTimeStamp(iCount) {
  13037. const ts = Date.now();
  13038. if (iCount !== 0) {
  13039. iCount = iCount + 1;
  13040. return ts - (ts % iCount) + iCount;
  13041. } else {
  13042. return ts;
  13043. }
  13044. }
  13045.  
  13046. /**
  13047. * 讯飞听见翻译
  13048. * @param {String} text 要翻译的文本
  13049. * @returns {Promise<TransRawData>} 翻译结果对象
  13050. */
  13051. async function translate_iflyrec(text) {
  13052. const options = {
  13053. method: "POST",
  13054. url: 'https://www.iflyrec.com/TranslationService/v1/textTranslation',
  13055. data: JSON.stringify({
  13056. "from": "2",
  13057. "to": getTargetLanguage('iflyrec'),
  13058. "contents": [{
  13059. "text": text,
  13060. "frontBlankLine": 0
  13061. }]
  13062. }),
  13063. anonymous: true,
  13064. headers: {
  13065. 'Content-Type': 'application/json',
  13066. 'Origin': 'https://www.iflyrec.com',
  13067. },
  13068. responseType: "json",
  13069. };
  13070. return await BaseTranslate(options, res => JSON.parse(res).biz[0].translateResult.replace(/\\n/g, "\n\n"));
  13071. }
  13072.  
  13073. /**
  13074. * 有道翻译
  13075. * @param {string} raw 原文
  13076. * @returns {Promise<TransRawData>} 翻译结果对象
  13077. */
  13078. async function translate_youdao_mobile(raw) {
  13079. /**
  13080. * 生成cookie
  13081. */
  13082. const getcookie = (() => {
  13083. // 生成IP地址
  13084. const generateIP = () => {
  13085. return `${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}.${OJB_getRandomNumberInRange(1, 255)}`;
  13086. }
  13087. // 生成OUTFOX_SEARCH_USER_ID_NCOO的值
  13088. const OUTFOX_SEARCH_USER_ID_NCOO = `${OJB_getRandomNumberInRange(100000000, 999999999)}.${OJB_getRandomNumberInRange(100000000, 999999999)}`;
  13089. // 生成OUTFOX_SEARCH_USER_ID的值
  13090. const OUTFOX_SEARCH_USER_ID = `${OJB_getRandomNumberInRange(100000000, 999999999)}@${generateIP()}`;
  13091. return `OUTFOX_SEARCH_USER_ID_NCOO=${OUTFOX_SEARCH_USER_ID_NCOO}; OUTFOX_SEARCH_USER_ID=${OUTFOX_SEARCH_USER_ID}`;
  13092. })();
  13093.  
  13094. /**
  13095. * 生成随机时间戳
  13096. */
  13097. const gettime = (new Date()).getTime();
  13098.  
  13099. /**
  13100. * 生成sign
  13101. */
  13102. const getsign = (() => {
  13103. const d = "fanyideskweb";
  13104. const u = "webfanyi";
  13105. const t = "fsdsogkndfokasodnaso";
  13106. function A(e) {
  13107. return CryptoJS.MD5(e.toString()).toString(CryptoJS.enc.Hex);
  13108. }
  13109. function w(e) {
  13110. return A(`client=${d}&mysticTime=${e}&product=${u}&key=${t}`);
  13111. }
  13112. return w(gettime);
  13113. })();
  13114.  
  13115. /**
  13116. * 解码方法
  13117. * @param {string} src 待解码的字符串
  13118. * @returns {Object} 解码后的数据
  13119. */
  13120. const decode = function (src) {
  13121. // 解码URL安全的Base64
  13122. const decodeUrlSafeBase64 = (str) => {
  13123. let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  13124. return base64;
  13125. }
  13126.  
  13127. // 使用MD5生成key和iv,取前16字节
  13128. const key = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl";
  13129. const iv = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4";
  13130. const keyHash = CryptoJS.MD5(key).toString();
  13131. const ivHash = CryptoJS.MD5(iv).toString();
  13132.  
  13133. // 使用AES-128-CBC模式进行解密
  13134. const keyForAES = CryptoJS.enc.Hex.parse(keyHash).toString().substring(0, 32);
  13135. const ivForAES = CryptoJS.enc.Hex.parse(ivHash).toString().substring(0, 32);
  13136.  
  13137. // 解码URL安全的Base64
  13138. const decodedBase64 = decodeUrlSafeBase64(src);
  13139.  
  13140. // 解密
  13141. const decrypted = CryptoJS.AES.decrypt({
  13142. ciphertext: CryptoJS.enc.Base64.parse(decodedBase64)
  13143. }, CryptoJS.enc.Hex.parse(keyForAES), {
  13144. iv: CryptoJS.enc.Hex.parse(ivForAES),
  13145. mode: CryptoJS.mode.CBC,
  13146. padding: CryptoJS.pad.Pkcs7
  13147. });
  13148.  
  13149. // 将解密结果转换为Utf8字符串
  13150. const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
  13151.  
  13152. // 处理JSON字符串并解析
  13153. const jsonStr = decryptedStr.substring(0, decryptedStr.lastIndexOf("}") + 1);
  13154. return JSON.parse(jsonStr);
  13155. }
  13156. // 整理数据
  13157. const organizeTranslation = (data) => {
  13158. // 提取translateResult数组
  13159. const { translateResult } = data;
  13160.  
  13161. // 整理tgt字段
  13162. return translateResult
  13163. .flat()
  13164. .map(item => item.tgt)
  13165. .join('');
  13166. };
  13167. // 表单数据
  13168. const data = {
  13169. "i": raw,
  13170. "from": "auto",
  13171. "to": getTargetLanguage('youdao'),
  13172. "dictResult": "true",
  13173. "keyid": "webfanyi",
  13174. "sign": getsign,
  13175. "client": "fanyideskweb",
  13176. "product": "webfanyi",
  13177. "appVersion": "1.0.0",
  13178. "vendor": "web",
  13179. "pointParam": "client,mysticTime,product",
  13180. "mysticTime": gettime,
  13181. "keyfrom": "fanyi.web",
  13182. "mid": "1",
  13183. "screen": "1",
  13184. "model": "1",
  13185. "network": "wifi",
  13186. "abtest": "0",
  13187. "yduuid": "abcdefg"
  13188. };
  13189. const options = {
  13190. method: "POST",
  13191. url: 'https://dict.youdao.com/webtranslate',
  13192. data: new URLSearchParams(data),
  13193. anonymous: true,
  13194. cookie: getcookie,
  13195. headers: {
  13196. "Content-Type": "application/x-www-form-urlencoded",
  13197. 'Referer': 'https://fanyi.youdao.com/',
  13198. }
  13199. }
  13200. return await BaseTranslate(options,
  13201. res => {
  13202. const decodeData = decode(res)
  13203. const result = organizeTranslation(decodeData);
  13204. return result;
  13205. }
  13206. );
  13207. }
  13208.  
  13209. /**
  13210. * google翻译
  13211. * @param {string} raw 原文
  13212. * @returns {Promise<TransRawData>} 翻译结果对象
  13213. */
  13214. async function translate_gg(raw) {
  13215. const params = `tl=${getTargetLanguage('google')}&q=${encodeURIComponent(raw)}`;
  13216. const options = {
  13217. method: "GET",
  13218. url: `https://translate.google.com/m?${params}`,
  13219. }
  13220. return await BaseTranslate(options,
  13221. res => $(res).filter('.result-container').text() || $(res).find('.result-container').text());
  13222. }
  13223.  
  13224. /**
  13225. * 彩云翻译
  13226. * @param {string} raw 原文
  13227. * @returns {Promise<TransRawData>} 翻译结果对象
  13228. */
  13229. async function translate_caiyun(raw) {
  13230. const source = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
  13231. const dic = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"].reduce((dic, current, index) => { dic[current] = source[index]; return dic }, {});
  13232. const browser_id = CryptoJS.MD5(Math.random().toString()).toString();
  13233. const caiyun_jwt = await (async () => {
  13234. const options = {
  13235. method: "POST",
  13236. url: 'https://api.interpreter.caiyunai.com/v1/user/jwt/generate',
  13237. headers: {
  13238. "content-type": "application/json",
  13239. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  13240. "origin": "https://fanyi.caiyunapp.com",
  13241. },
  13242. data: JSON.stringify({ browser_id }),
  13243. }
  13244. const res = await OJB_GMRequest(options);
  13245. return JSON.parse(res.responseText).jwt;
  13246. })();
  13247.  
  13248. // 解码
  13249. const decodeUnicode = str => {
  13250. const decoder = new TextDecoder();
  13251. const data = Uint8Array.from(atob(str), c => c.charCodeAt(0));
  13252. return decoder.decode(data);
  13253. };
  13254. const decoder = line => decodeUnicode([...line].map(i => dic[i] || i).join(""));
  13255.  
  13256. const options = {
  13257. method: "POST",
  13258. url: 'https://api.interpreter.caiyunai.com/v1/translator',
  13259. data: JSON.stringify({
  13260. "source": raw.split('\n'),
  13261. "browser_id": browser_id,
  13262. "trans_type": getTargetLanguage('caiyun'),
  13263. "request_id": "web_fanyi",
  13264. "media": "text",
  13265. "os_type": "web",
  13266. "dict": true,
  13267. "cached": true,
  13268. "replaced": true,
  13269. "style": "formal",
  13270. "model": "",
  13271. "detect": true,
  13272. }),
  13273. headers: {
  13274. "content-type": "application/json;charset=UTF-8",
  13275. "x-authorization": "token:qgemv4jr1y38jyq6vhvi",
  13276. "t-authorization": caiyun_jwt
  13277. }
  13278. }
  13279. return await BaseTranslate(options, res => JSON.parse(res).target.map(decoder).join('\n'))
  13280. }
  13281.  
  13282. /**
  13283. * ChatGPT
  13284. * @param {string} raw 原文
  13285. * @returns {Promise<TransRawData>} 翻译结果对象
  13286. */
  13287. async function translate_openai(raw) {
  13288. const modelDefault = 'gpt-3.5-turbo';
  13289. const lang = getTargetLanguage('openai');
  13290. let prompt = "";
  13291. if (OJBetter.chatgpt.customPrompt) {
  13292. prompt = `\n${OJBetter.chatgpt.customPrompt}`;
  13293. if (!OJBetter.chatgpt.asSystemPrompt) {
  13294. prompt += `\n${raw}`;
  13295. };
  13296. } else {
  13297. prompt = `
  13298. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13299. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13300. ? "keeping the LaTeX equations unchanged."
  13301. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13302. }
  13303. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13304. What I need is a carefully polished ${lang} translation of my question segment. ${OJBetter.chatgpt.asSystemPrompt ? '' :
  13305. `The segment to be translated is as follows: "
  13306. ${raw}
  13307. "`}`;
  13308. };
  13309. const data = {
  13310. model: OJBetter.chatgpt.config.model || modelDefault,
  13311. messages: OJBetter.chatgpt.asSystemPrompt ?
  13312. [
  13313. {
  13314. role: "system",
  13315. content: prompt
  13316. },
  13317. {
  13318. role: "user",
  13319. content: raw
  13320. }
  13321. ] :
  13322. [
  13323. {
  13324. role: "user",
  13325. content: prompt
  13326. }
  13327. ],
  13328. temperature: 0.7,
  13329. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13330. };
  13331. const options = {
  13332. method: "POST",
  13333. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13334. data: JSON.stringify(data),
  13335. responseType: 'json',
  13336. headers: {
  13337. 'Content-Type': 'application/json',
  13338. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13339. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13340. }
  13341. }
  13342. return await BaseTranslate(options,
  13343. res => res,
  13344. undefined,
  13345. response => response.response.choices[0].message.content);
  13346. }
  13347.  
  13348. /**
  13349. * ChatGPT 流式传输
  13350. * @param {string} raw 原文
  13351. * @param {TranslateDiv} translateDiv 翻译结果面板
  13352. * @returns {Promise<TransRawData>} 翻译结果对象
  13353. */
  13354. async function translate_openai_stream(raw, translateDiv) {
  13355. const result = {
  13356. done: true,
  13357. checkPassed: null,
  13358. response: null,
  13359. responseText: null,
  13360. text: "",
  13361. error: null,
  13362. message: null
  13363. };
  13364. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13365. try {
  13366. for await (const delta of openai_stream(raw)) {
  13367. result.text += delta;
  13368. // 翻译结果面板更新
  13369. translateDiv.updateTranslateDiv(result.text, !((OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru) && (!OJBetter.translation.forceTurndownConversion)), false);
  13370. }
  13371. return result;
  13372. } catch (err) {
  13373. console.warn(err);
  13374. result.error = {
  13375. message: err.message || null,
  13376. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13377. enumerable: err,
  13378. source: 'openai_stream'
  13379. };
  13380. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13381. }
  13382.  
  13383. return result;
  13384. }
  13385.  
  13386. /**
  13387. * 流式传输
  13388. * @param {string} raw 原文
  13389. * @returns {AsyncGenerator<string>} 返回 AsyncGenerator
  13390. */
  13391. async function* openai_stream(raw) {
  13392. const modelDefault = 'gpt-3.5-turbo';
  13393. const lang = getTargetLanguage('openai');
  13394. let prompt = "";
  13395. if (OJBetter.chatgpt.customPrompt) {
  13396. prompt = `\n${OJBetter.chatgpt.customPrompt}`;
  13397. if (!OJBetter.chatgpt.asSystemPrompt) {
  13398. prompt += `\n${raw}`;
  13399. };
  13400. } else {
  13401. prompt = `
  13402. As a professional English translator, your task is to accurately translate a segment of an algorithm programming competition question into ${lang}.
  13403. The translation should use professional terms and maintain the text format, including ${OJBetter.typeOfPage.is_oldLatex || OJBetter.typeOfPage.is_acmsguru
  13404. ? "keeping the LaTeX equations unchanged."
  13405. : "keeping the brackets【】, HTML tags, and their content unchanged."
  13406. }
  13407. After translation, please ensure that the ${lang} version conforms to normal expression habits.
  13408. What I need is a carefully polished ${lang} translation of my question segment. ${OJBetter.chatgpt.asSystemPrompt ? '' :
  13409. `The segment to be translated is as follows: "
  13410. ${raw}
  13411. "`}`;
  13412. };
  13413. const data = {
  13414. model: OJBetter.chatgpt.config.model || modelDefault,
  13415. messages: OJBetter.chatgpt.asSystemPrompt ?
  13416. [
  13417. {
  13418. role: "system",
  13419. content: prompt
  13420. },
  13421. {
  13422. role: "user",
  13423. content: raw
  13424. }
  13425. ] :
  13426. [
  13427. {
  13428. role: "user",
  13429. content: prompt
  13430. }
  13431. ],
  13432. temperature: 0.7,
  13433. stream: true,
  13434. ...Object.assign({}, ...OJBetter.chatgpt.config.data)
  13435. };
  13436. const options = {
  13437. method: "POST",
  13438. url: OJBetter.chatgpt.config.proxy || 'https://api.openai.com/v1/chat/completions',
  13439. data: JSON.stringify(data),
  13440. responseType: 'stream',
  13441. headers: {
  13442. 'Content-Type': 'application/json',
  13443. 'Authorization': 'Bearer ' + OJBetter.chatgpt.config.key,
  13444. ...Object.assign({}, ...OJBetter.chatgpt.config.header)
  13445. }
  13446. }
  13447. const response = await OJB_GMRequest(options, true);
  13448. const reader = response.response.getReader();
  13449. const decoder = new TextDecoder();
  13450. let buffer = ''; // 用于累积数据片段的缓冲区
  13451.  
  13452. while (true) {
  13453. const { done, value } = await reader.read();
  13454. if (done) break;
  13455. buffer += decoder.decode(value, { stream: true }); // 将新的数据片段追加到缓冲区
  13456. let lines = buffer.split("\n\n"); // 处理累积的数据
  13457.  
  13458. // 缓冲区的最后一行可能还未完整接收,保留在缓冲区中,-1
  13459. for (let i = 0; i < lines.length - 1; i++) {
  13460. let line = lines[i];
  13461. line = line.substring(5); // 移除 'data:' 前缀
  13462. if (line.includes('[DONE]')) {
  13463. return; // End
  13464. }
  13465. try {
  13466. let data = JSON.parse(line);
  13467. let delta = data['choices'][0]['delta'];
  13468. let content = delta['content'] ? delta['content'] : "";
  13469. yield content; // 传递数据给调用者
  13470. } catch (error) {
  13471. console.warn(`Error parsing JSON: ${error}\n\nError data: ${line}`);
  13472. }
  13473. }
  13474.  
  13475. // 保留最后一行在缓冲区中
  13476. buffer = lines.slice(-1);
  13477. }
  13478.  
  13479. return buffer;
  13480. }
  13481.  
  13482. /**
  13483. * @typedef {Object} CheckResponseResult
  13484. * @property {boolean} status 检查是否通过
  13485. * @property {string} message 检查失败时的消息
  13486. */
  13487.  
  13488. /**
  13489. * @typedef {Object} ErrorResponse
  13490. * @property {Object} message 错误消息
  13491. * @property {Object} stack 错误堆栈
  13492. * @property {Object} enumerable 可枚举的错误属性
  13493. * @property {string} source 错误来源
  13494. */
  13495.  
  13496. /**
  13497. * @typedef {Object} TransRawData
  13498. * @property {boolean} done 操作是否完成
  13499. * @property {CheckResponseResult|null} checkPassed 检查是否通过的结果
  13500. * @property {Object|null} response 响应对象
  13501. * @property {string|null} text 处理后的文本
  13502. * @property {ErrorResponse} error 错误列表
  13503. * @property {string|null} message 可能的消息
  13504. */
  13505.  
  13506. /**
  13507. * 通用翻译函数
  13508. * @param {Object} options GM_xmlhttpRequest 的参数
  13509. * @param {Function} processer 响应再处理函数,它接收响应文本,并应返回处理后的文本。
  13510. * @param {Function} checkResponse 检查文本是否符合预期的函数,它接收文本,并返回一个Object,包含状态和信息。默认为返回 { status: true, message: 'ok' }
  13511. * @param {Function} getResponseText 重写响应文本获取函数,它接收response,并返回响应文本。 默认为 response.responseText
  13512. * @returns {Promise<TransRawData>} 返回 Promise,其解析值为翻译结果对象
  13513. */
  13514. async function BaseTranslate(options, processer, checkResponse = () => { return { status: true, message: 'ok' } }, getResponseText = (response) => response.responseText) {
  13515. const result = {
  13516. done: false,
  13517. checkPassed: null,
  13518. response: null,
  13519. responseText: null,
  13520. text: "",
  13521. error: null,
  13522. message: null
  13523. };
  13524. const helpText = i18next.t('error.basic', { ns: 'translator' }); // 基本帮助提示信息
  13525. const toDo = async () => {
  13526. try {
  13527. result.response = await OJB_GMRequest(options);
  13528. result.responseText = result.response.responseText;
  13529. result.text = getResponseText(result.response);
  13530. } catch (err) {
  13531. console.warn(err);
  13532. result.error = {
  13533. message: err.message || null,
  13534. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13535. enumerable: err,
  13536. source: 'GMRequest'
  13537. };
  13538. result.message = `${i18next.t('error.GMRequest', { ns: 'translator' })}${helpText}`;
  13539. throw result;
  13540. }
  13541. try {
  13542. result.text = processer(result.text);
  13543. } catch (err) {
  13544. console.warn(err);
  13545. result.error = {
  13546. message: err.message || null,
  13547. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13548. enumerable: err,
  13549. source: 'processer'
  13550. };
  13551. result.message = `${i18next.t('error.processer', { ns: 'translator' })}${helpText}`;
  13552. throw result;
  13553. }
  13554. try {
  13555. result.checkPassed = checkResponse(result.text);
  13556. if (result.checkPassed.status) result.done = true;
  13557. else result.message = result.checkPassed.message;
  13558. return result;
  13559. } catch (err) {
  13560. console.warn(err);
  13561. result.error = {
  13562. message: err.message || null,
  13563. stack: err.stack ? err.stack.replace(/\n/g, '<br>').replace(/\s/g, '&nbsp;') : null,
  13564. enumerable: err,
  13565. source: 'checkResponse'
  13566. };
  13567. result.message = `${i18next.t('error.checkResponse', { ns: 'translator' })}${helpText}`;
  13568. throw result;
  13569. }
  13570. };
  13571.  
  13572. return await OJB_promiseRetryWrapper(toDo, {
  13573. maxRetries: 3,
  13574. errorHandler: (err, maxRetries, attemptsLeft) => {
  13575. const detailedError = {
  13576. maxRetries: maxRetries,
  13577. attemptsLeft: attemptsLeft,
  13578. ...err
  13579. };
  13580. return detailedError;
  13581. }
  13582. });
  13583. }
  13584.  
  13585. /**
  13586. * 查询服务余额
  13587. * @param {Object} quotaConfig - 配额配置对象
  13588. * @returns {Promise} 返回包含余额信息的 Promise
  13589. */
  13590. async function queryServerBalance(quotaConfig) {
  13591. // 确保传入了有效的配置对象
  13592. if (!quotaConfig || !quotaConfig.url) {
  13593. return Promise.reject(new Error('Quota configuration is missing.'));
  13594. }
  13595.  
  13596. // 准备请求选项
  13597. const requestOptions = {
  13598. method: quotaConfig.method || 'GET',
  13599. url: quotaConfig.url,
  13600. headers: {
  13601. ...Object.assign({}, ...quotaConfig.header)
  13602. },
  13603. data: JSON.stringify({ ...Object.assign({}, ...quotaConfig.data) })
  13604. };
  13605.  
  13606. // 发送请求并返回 Promise
  13607. return OJB_GMRequest(requestOptions).then(response => {
  13608. try {
  13609. const responseData = JSON.parse(response.responseText);
  13610. // 从响应数据中提取余额
  13611. const surplusPath = quotaConfig.surplus;
  13612. const surplusValue = OJB_evaluatePathOrExpression(responseData, surplusPath);
  13613. return surplusValue;
  13614. } catch (error) {
  13615. return Promise.reject(new Error('Failed to parse balance response.'));
  13616. }
  13617. }).catch(error => {
  13618. console.warn('Error querying balance:', error);
  13619. return Promise.reject(error);
  13620. });
  13621. }
  13622.  
  13623. /**
  13624. * 确认 jQuery 已加载
  13625. * @param {number} retryDelay 重试延迟(毫秒)
  13626. * @returns {Promise<void>}
  13627. */
  13628. async function ensureJQueryIsLoaded(retryDelay = 50) {
  13629. while (typeof jQuery === 'undefined') {
  13630. console.warn(`JQuery is not loaded. Retry after ${retryDelay} ms.`);
  13631. await OJB_delay(retryDelay);
  13632. retryDelay = Math.min(retryDelay * 2, 2000);
  13633. }
  13634. }
  13635.  
  13636. /**
  13637. * 加载必须的函数
  13638. * @returns {Promise} 加载提示信息
  13639. */
  13640. async function loadRequiredFunctions() {
  13641. await initVar();// 初始化全局变量
  13642. return Promise.allSettled([
  13643. initDB(), // 连接数据库
  13644. initI18next(), // i18next初始化
  13645. initButtonFunc(), // 加载按钮相关函数
  13646. initHTML2MarkDown(), // 初始化html2markdown转换器
  13647. checkScriptVersion(), // 更新检查
  13648. ...(OJBetter.typeOfPage.is_acmsguru ? [acmsguruReblock()] : []) // 为acmsguru题面重新划分div
  13649. ]);
  13650. }
  13651.  
  13652. /**
  13653. * DOM加载后即可执行
  13654. */
  13655. function initOnDOMReady() {
  13656. showAnnounce(); // 显示公告
  13657. showWarnMessage(); // 显示警告消息
  13658. initSettingsPanel(); // 加载设置按钮面板
  13659. initMonacoEditor(); // 初始化monaco编辑器资源
  13660. localizeWebsite(); // 网站本地化替换
  13661. addDependencyStyles(); // 添加一些依赖库的样式
  13662. addI18nStyles(); // 添加包含i18n内容的样式
  13663. if (OJBetter.basic.expandFoldingblocks) ExpandFoldingblocks(); // 折叠块展开
  13664. if (OJBetter.basic.renderPerfOpt) RenderPerfOpt(); // 折叠块渲染优化
  13665. if (OJBetter.basic.selectElementPerfOpt) SelectElementPerfOpt(); // 下拉选择框性能优化
  13666. if (OJBetter.typeOfPage.is_problem) {
  13667. const problemPageLinkbar = new ProblemPageLinkbar(); // 创建题目页相关链接栏
  13668. if (OJBetter.basic.showCF2vjudge) CF2vjudge(problemPageLinkbar); // 跳转到Vjudge按钮
  13669. if (OJBetter.basic.showJumpToLuogu) CF2luogu(problemPageLinkbar); // 跳转到洛谷按钮
  13670. if (OJBetter.clist.enabled.problem) showRatingByClist_problem(problemPageLinkbar); // problem页显示Rating
  13671. }
  13672. if (OJBetter.typeOfPage.is_contest) {
  13673. if (OJBetter.clist.enabled.contest) showRatingByClist_contest(); // contest页显示Rating
  13674. }
  13675. if (OJBetter.typeOfPage.is_problemset) {
  13676. if (OJBetter.clist.enabled.problemset) showRatingByClist_problemset(); // problemset页显示Rating
  13677. }
  13678. if (OJBetter.typeOfPage.is_problem && OJBetter.monaco.enableOnProblemPage) {
  13679. addProblemPageCodeEditor(); // 添加题目页代码编辑器
  13680. }
  13681. }
  13682.  
  13683. /**
  13684. * 需要在页面资源完全加载后执行的函数
  13685. */
  13686. function onResourcesReady(loadingMessage) {
  13687. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadFunc', { ns: 'alert' })}`);
  13688. initializeInParallel(loadingMessage);
  13689. initializeSequentially(loadingMessage);
  13690. }
  13691.  
  13692. /**
  13693. * 可以异步并行的函数
  13694. */
  13695. function initializeInParallel(loadingMessage) {
  13696. if (OJBetter.basic.darkMode == "dark") darkModeStyleAdjustment(); // 黑暗模式额外的处理事件
  13697. if (OJBetter.basic.commentPaging) CommentPagination(); // 评论区分页
  13698. if (OJBetter.translation.comment.transMode == "2") multiChoiceTranslation(); // 选段翻译支持
  13699. if (OJBetter.monaco.beautifyPreBlocks) beautifyPreBlocksWithMonaco(); // 美化Pre代码块
  13700. if (OJBetter.basic.hiddenProblemTag) hiddenProblemTag()// 隐藏题目问题标签
  13701. }
  13702.  
  13703. /**
  13704. * 必须按序执行的函数
  13705. */
  13706. async function initializeSequentially(loadingMessage) {
  13707. await addConversionButton(); // 添加MD/复制/翻译按钮
  13708. if ((OJBetter.typeOfPage.is_problem || OJBetter.typeOfPage.is_completeProblemset) && OJBetter.translation.memory.enabled) {
  13709. await initTransResultsRecover(); // 翻译结果恢复功能初始化
  13710. }
  13711. if (OJBetter.translation.auto.enabled) {
  13712. await initTransWhenViewable(); // 自动翻译
  13713. }
  13714. if (OJBetter.basic.standingsRecolor && OJBetter.typeOfPage.is_cfStandings) {
  13715. await recolorStandings(); // cf赛制榜单重新着色
  13716. }
  13717. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('loadSuccess', { ns: 'alert' })}`, 'success', 3000);
  13718. }
  13719.  
  13720. /**
  13721. * 主方法
  13722. */
  13723. async function main() {
  13724. await ensureJQueryIsLoaded(); // 等待jQuery加载
  13725. const loadingMessage = new LoadingMessage();
  13726. await loadRequiredFunctions(); // 加载必须的函数
  13727. initOnDOMReady(); // DOM加载后即可执行的函数
  13728. if (OJBetter.preference.showLoading) loadingMessage.updateStatus(`${OJBetter.state.name} —— ${i18next.t('onload', { ns: 'alert' })}`);
  13729.  
  13730. // 检查页面资源是否已经完全加载
  13731. if (OJBetter.state.notWaiteLoaded) {
  13732. onResourcesReady(loadingMessage);
  13733. } else {
  13734. if (document.readyState === 'complete') {
  13735. onResourcesReady(loadingMessage);
  13736. } else {
  13737. window.addEventListener('load', () => onResourcesReady(loadingMessage));
  13738. }
  13739. }
  13740. };
  13741.  
  13742. // ------------------------------
  13743. // 脚本加载入口
  13744. if (document.readyState === 'loading') {
  13745. document.addEventListener("DOMContentLoaded", main);
  13746. } else {
  13747. main(); // 如果DOMContentLoaded已经触发,立即执行
  13748. }
  13749. // ------------------------------
  13750.  
  13751. // ------------------------------
  13752. // 配置自动迁移代码(将在10个小版本后移除-1.83)
  13753. // ------------------------------
  13754.  
  13755. {
  13756. let bottomZh_CN = GM_getValue("bottomZh_CN");
  13757. if (bottomZh_CN !== undefined) {
  13758. if (bottomZh_CN == true) {
  13759. GM_setValue("localizationLanguage", "zh");
  13760. } else {
  13761. GM_setValue("localizationLanguage", "initial");
  13762. }
  13763. GM_deleteValue("bottomZh_CN");
  13764. location.reload();
  13765. }
  13766. }
  13767. {
  13768. let config = GM_getValue("chatgpt-config");
  13769. if (config && config !== undefined) {
  13770. let index = parseInt(config.choice, 10);
  13771. if (index == -1) config.choice = "";
  13772. else config.choice = config.configurations[index].note;
  13773. config.configurations.forEach(function (item) {
  13774. item.name = item.note;
  13775. delete item.note;
  13776. });
  13777. GM_deleteValue("chatgpt-config");
  13778. GM_setValue("chatgpt_config", config);
  13779. location.reload();
  13780. }
  13781. }
  13782. {
  13783. let config = GM_getValue("Complet_config");
  13784. if (config && config.changed === undefined) {
  13785. config.changed = true; // 设置一个迁移标志
  13786. config.configurations.forEach(function (item) {
  13787. if (item.note !== undefined) {
  13788. item.name = item.note;
  13789. delete item.note;
  13790. }
  13791. });
  13792. GM_setValue("Complet_config", config);
  13793. location.reload();
  13794. }
  13795. }