Greasy Fork is available in English.

IAI OJ Downloader

Download OJ problems as markdown files

  1. // ==UserScript==
  2. // @name IAI OJ Downloader
  3. // @namespace iai-sh-cn
  4. // @version 0.2
  5. // @description Download OJ problems as markdown files
  6. // @license AGPL-3.0-or-later
  7. // @author Y.V
  8. // @match https://iai.sh.cn/*
  9. // @icon https://iai.sh.cn/images/logo.png
  10. // @grant GM_xmlhttpRequest
  11. // @require https://code.jquery.com/jquery-3.6.0.min.js
  12. // @require https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // ---------------------------- 常量定义 ----------------------------
  20. const BUTTON_ID = 'download-button';
  21. const BUTTON_TEXT_DEFAULT = '下载题目';
  22. const BUTTON_TEXT_LOADING = '加载中...';
  23. const BUTTON_STYLE = {
  24. 'position': 'fixed',
  25. 'top': '10px',
  26. 'right': '10px',
  27. 'width': '100px',
  28. 'height': '40px',
  29. 'border': 'none',
  30. 'border-radius': '5px',
  31. 'background': '#00a0e9',
  32. 'color': '#fff',
  33. 'fontSize': '16px',
  34. 'fontWeight': 'bold',
  35. 'cursor': 'pointer'
  36. };
  37. const API_ENDPOINT = 'https://api.iai.sh.cn/contest/listProblem';
  38.  
  39. // ---------------------------- 函数定义 ----------------------------
  40.  
  41. /**
  42. * 创建下载按钮并添加到页面。
  43. * @returns {JQuery<HTMLElement>} 下载按钮的 jQuery 对象。
  44. */
  45. function createDownloadButton() {
  46. const button = $(`<button id="${BUTTON_ID}">${BUTTON_TEXT_DEFAULT}</button>`);
  47. button.css(BUTTON_STYLE);
  48. $('body').append(button);
  49. return button;
  50. }
  51.  
  52. /**
  53. * 从当前 URL 中提取比赛 ID。
  54. * @returns {string|null} 比赛 ID,如果提取失败则返回 null。
  55. */
  56. function getContestIdFromUrl() {
  57. const urlParts = window.location.href.split('/');
  58. return urlParts[4] || null;
  59. }
  60.  
  61. /**
  62. * 从 API 获取题目数据。
  63. * @param {string} contestId 比赛 ID。
  64. * @returns {Promise<Array>} 包含题目数据的 Promise。
  65. * @throws {Error} 如果 API 请求失败。
  66. */
  67. async function fetchProblemData(contestId) {
  68. const apiUrl = `${API_ENDPOINT}?contestId=${contestId}`;
  69. const response = await axios.get(apiUrl);
  70. return response.data;
  71. }
  72.  
  73. /**
  74. * 按等级对题目数据进行分组。
  75. * @param {Array} problems 题目数据数组。
  76. * @returns {Object} 按等级分组的题目对象。
  77. */
  78. function groupProblemsByLevel(problems) {
  79. return problems.reduce((groups, problem) => {
  80. const level = problem.level || '未分类';
  81. groups[level] = groups[level] || '未分级';
  82. groups[level].push(problem);
  83. return groups;
  84. }, {});
  85. }
  86.  
  87. /**
  88. * 生成单个题目的 Markdown 内容。
  89. * @param {Object} problem 题目对象。
  90. * @param {number} index 题目在当前分组中的索引。
  91. * @returns {string} 题目的 Markdown 内容。
  92. */
  93. function generateProblemMarkdown(problem, index) {
  94. let md = `### 题目 ${index + 1}\n\n`;
  95. md += `${problem.title}\n\n`;
  96. md += `#### 题目描述\n\n${problem.description}\n\n`;
  97. md += `#### 输入格式\n\n${problem.inputFormat}\n\n`;
  98. md += `#### 输出格式\n\n${problem.outputFormat}\n\n`;
  99. md += `#### 数据范围\n\n${problem.dataRange}\n\n`;
  100. md += `#### 样例数据\n\n`;
  101. problem.exampleList.forEach(example => {
  102. const input = example.input.replace(/\n/g, '\n> ');
  103. const output = example.output.replace(/\n/g, '\n> ');
  104. md += `**输入**\n\n> ${input}\n\n`;
  105. md += `**输出**\n\n> ${output}\n\n`;
  106. if (example.note) {
  107. md += `**说明**\n\n${example.note}\n\n`;
  108. }
  109. });
  110. md += '\n\n\n'; // 题目之间添加空行
  111. return md;
  112. }
  113.  
  114. /**
  115. * 处理下载逻辑。
  116. * @param {JQuery<HTMLElement>} button 下载按钮的 jQuery 对象。
  117. */
  118. async function handleDownload(button) {
  119. button.prop('disabled', true).text(BUTTON_TEXT_LOADING);
  120.  
  121. try {
  122. const contestId = getContestIdFromUrl();
  123. if (!contestId) {
  124. alert('无法从 URL 中获取比赛 ID。');
  125. return;
  126. }
  127.  
  128. const problemData = await fetchProblemData(contestId);
  129. const contestTitle = $('h2.ant-typography').text() || '题目';
  130. const zip = new JSZip();
  131. const problemsByLevel = groupProblemsByLevel(problemData);
  132.  
  133. for (const level in problemsByLevel) {
  134. let markdownContent = '';
  135. problemsByLevel[level].forEach((problem, index) => {
  136. markdownContent += generateProblemMarkdown(problem, index);
  137. });
  138. zip.file(`${level}.md`, markdownContent);
  139. }
  140.  
  141. const blob = await zip.generateAsync({ type: 'blob' });
  142. const link = document.createElement('a');
  143. link.href = URL.createObjectURL(blob);
  144. link.download = `${contestTitle}.zip`;
  145. link.click();
  146.  
  147. } catch (error) {
  148. console.error('下载题目失败:', error);
  149. alert('下载题目失败,请检查控制台错误信息。');
  150. } finally {
  151. button.prop('disabled', false).text(BUTTON_TEXT_DEFAULT);
  152. }
  153. }
  154.  
  155. // ---------------------------- 初始化 ----------------------------
  156. $(document).ready(function() {
  157. const downloadButton = createDownloadButton();
  158. downloadButton.click(() => handleDownload(downloadButton));
  159. });
  160.  
  161. })();