spoiled competitive programming

Parse AtCoder problem statement into sections and create Anki CSV

  1. // ==UserScript==
  2. // @name spoiled competitive programming
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3
  5. // @description Parse AtCoder problem statement into sections and create Anki CSV
  6. // @author Harui (totally helped by GPT-4o)
  7. // @match https://atcoder.jp/contests/*/tasks/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Create buttons for Japanese
  16. const openMarkdownButtonJa = document.createElement('button');
  17. openMarkdownButtonJa.textContent = '新しいタブで日本語のMarkdownを開く';
  18. openMarkdownButtonJa.style.position = 'fixed';
  19. openMarkdownButtonJa.style.left = '10px';
  20. openMarkdownButtonJa.style.bottom = '145px';
  21. openMarkdownButtonJa.style.zIndex = '1000';
  22. openMarkdownButtonJa.style.padding = '10px';
  23. openMarkdownButtonJa.style.backgroundColor = '#4CAF50';
  24. openMarkdownButtonJa.style.color = 'white';
  25. openMarkdownButtonJa.style.border = 'none';
  26. openMarkdownButtonJa.style.borderRadius = '5px';
  27. openMarkdownButtonJa.style.cursor = 'pointer';
  28.  
  29. const openHtmlButtonJa = document.createElement('button');
  30. openHtmlButtonJa.textContent = '新しいタブで日本語のHTMLを開く';
  31. openHtmlButtonJa.style.position = 'fixed';
  32. openHtmlButtonJa.style.left = '10px';
  33. openHtmlButtonJa.style.bottom = '100px';
  34. openHtmlButtonJa.style.zIndex = '1000';
  35. openHtmlButtonJa.style.padding = '10px';
  36. openHtmlButtonJa.style.backgroundColor = '#4CAF50';
  37. openHtmlButtonJa.style.color = 'white';
  38. openHtmlButtonJa.style.border = 'none';
  39. openHtmlButtonJa.style.borderRadius = '5px';
  40. openHtmlButtonJa.style.cursor = 'pointer';
  41.  
  42. // Append the buttons to the body
  43. document.body.appendChild(openMarkdownButtonJa);
  44. document.body.appendChild(openHtmlButtonJa);
  45.  
  46. // Event listener for open Markdown button (Japanese)
  47. openMarkdownButtonJa.addEventListener('click', () => {
  48. const markdownContent = htmlToMarkdown(extractHtml('ja'));
  49.  
  50. // Open the Markdown content in a new tab
  51. const newTab = window.open();
  52. newTab.document.open();
  53. newTab.document.write('<pre>' + markdownContent + '</pre>');
  54. newTab.document.close();
  55. });
  56.  
  57. // Event listener for open HTML button (Japanese)
  58. openHtmlButtonJa.addEventListener('click', () => {
  59. const htmlContent = htmlToAnkiHtml(extractHtml('ja'));
  60.  
  61. // Open the HTML content in a new tab
  62. const newTab = window.open();
  63. newTab.document.open();
  64. newTab.document.write(htmlContent);
  65. newTab.document.close();
  66. });
  67.  
  68. // Create buttons and style them
  69. const openMarkdownButtonEn = document.createElement('button');
  70. openMarkdownButtonEn.textContent = 'Open English Markdown in New Tab';
  71. openMarkdownButtonEn.style.position = 'fixed';
  72. openMarkdownButtonEn.style.left = '10px';
  73. openMarkdownButtonEn.style.bottom = '55px';
  74. openMarkdownButtonEn.style.zIndex = '1000';
  75. openMarkdownButtonEn.style.padding = '10px';
  76. openMarkdownButtonEn.style.backgroundColor = '#4CAF50';
  77. openMarkdownButtonEn.style.color = 'white';
  78. openMarkdownButtonEn.style.border = 'none';
  79. openMarkdownButtonEn.style.borderRadius = '5px';
  80. openMarkdownButtonEn.style.cursor = 'pointer';
  81.  
  82. const openHtmlButtonEn = document.createElement('button');
  83. openHtmlButtonEn.textContent = 'Open English HTML in New Tab';
  84. openHtmlButtonEn.style.position = 'fixed';
  85. openHtmlButtonEn.style.left = '10px';
  86. openHtmlButtonEn.style.bottom = '10px';
  87. openHtmlButtonEn.style.zIndex = '1000';
  88. openHtmlButtonEn.style.padding = '10px';
  89. openHtmlButtonEn.style.backgroundColor = '#4CAF50';
  90. openHtmlButtonEn.style.color = 'white';
  91. openHtmlButtonEn.style.border = 'none';
  92. openHtmlButtonEn.style.borderRadius = '5px';
  93. openHtmlButtonEn.style.cursor = 'pointer';
  94.  
  95. // Append the buttons to the body
  96. document.body.appendChild(openMarkdownButtonEn);
  97. document.body.appendChild(openHtmlButtonEn);
  98.  
  99. // Create button for Anki CSV download
  100. const downloadCsvButton = document.createElement('button');
  101. downloadCsvButton.textContent = 'Anki用CSVをダウンロード';
  102. downloadCsvButton.style.position = 'fixed';
  103. downloadCsvButton.style.left = '10px';
  104. downloadCsvButton.style.bottom = '190px';
  105. downloadCsvButton.style.zIndex = '1000';
  106. downloadCsvButton.style.padding = '10px';
  107. downloadCsvButton.style.backgroundColor = '#4CAF50';
  108. downloadCsvButton.style.color = 'white';
  109. downloadCsvButton.style.border = 'none';
  110. downloadCsvButton.style.borderRadius = '5px';
  111. downloadCsvButton.style.cursor = 'pointer';
  112.  
  113. // Append the button to the body
  114. document.body.appendChild(downloadCsvButton);
  115.  
  116. // Event listener for download CSV button
  117. downloadCsvButton.addEventListener('click', () => {
  118. const htmlContent = htmlToAnkiHtml(extractHtml('ja'));
  119. const csvContent = generateAnkiCsv(htmlContent);
  120.  
  121. // Create a blob from the CSV content
  122. const blob = new Blob([csvContent], { type: 'text/csv' });
  123. const url = URL.createObjectURL(blob);
  124.  
  125. // Create a link element and trigger the download
  126. const a = document.createElement('a');
  127. a.href = url;
  128. a.download = 'anki.csv';
  129. document.body.appendChild(a);
  130. a.click();
  131. document.body.removeChild(a);
  132. URL.revokeObjectURL(url);
  133. });
  134.  
  135. // Function to extract parts and remove Katex, returning HTML
  136. function extractHtml(lang) {
  137. const parts = document.querySelectorAll(`span.lang-${lang}`);
  138. const problemTitle = document.title;
  139.  
  140. let htmlContent = `<h1>${problemTitle}</h1>\n\n`;
  141.  
  142. parts.forEach(part => {
  143. // Clone the part to avoid modifying the original document
  144. const clone = part.cloneNode(true);
  145.  
  146. // Remove "Copy" buttons
  147. const copyButtons = clone.querySelectorAll('.btn-copy, .btn-pre, .div-btn-copy');
  148. copyButtons.forEach(button => button.remove());
  149.  
  150. // Remove Katex elements
  151. const katexElements = clone.querySelectorAll('.katex');
  152. katexElements.forEach(katex => {
  153. const tex = katex.querySelector('.katex-mathml annotation');
  154. if (tex) {
  155. const textNode = document.createTextNode(tex.textContent);
  156. katex.parentNode.replaceChild(textNode, katex);
  157. }
  158. });
  159.  
  160. // Append HTML content
  161. htmlContent += clone.outerHTML + '\n\n';
  162. });
  163.  
  164. return htmlContent;
  165. }
  166.  
  167. // Function to generate Anki CSV
  168. function generateAnkiCsv(htmlContent) {
  169. const problemId = window.location.pathname.split('/')[2];
  170. const problemAlpha = window.location.pathname.split('/')[4].split('_')[1];
  171. const problemTitle = document.querySelector('title').textContent.trim();
  172. const problemName = `${problemId.toUpperCase()}_${problemAlpha.toUpperCase()} ${problemTitle}`;
  173. const problemUrl = window.location.href;
  174.  
  175. // Replace double quotes with double double quotes
  176. const escapedHtmlContent = htmlContent.replace(/"/g, '""');
  177.  
  178. const csvContent = `#separator:tab\n#html:true\n"${problemName}"\t"<a href=""${problemUrl}"">${problemUrl}</a>"\t"${escapedHtmlContent}"`;
  179. return csvContent;
  180. }
  181.  
  182.  
  183. // Event listener for open Markdown button (English)
  184. openMarkdownButtonEn.addEventListener('click', () => {
  185. const markdownContent = htmlToMarkdown(extractHtml('en'));
  186.  
  187. // Open the Markdown content in a new tab
  188. const newTab = window.open();
  189. newTab.document.open();
  190. newTab.document.write('<pre>' + markdownContent + '</pre>');
  191. newTab.document.close();
  192. });
  193.  
  194. // Event listener for open HTML button (English)
  195. openHtmlButtonEn.addEventListener('click', () => {
  196. const htmlContent = extractHtml('en');
  197.  
  198. // Open the HTML content in a new tab
  199. const newTab = window.open();
  200. newTab.document.open();
  201. newTab.document.write(htmlContent);
  202. newTab.document.close();
  203. });
  204.  
  205. // Function to convert HTML to Markdown
  206. function htmlToMarkdown(html) {
  207. // Simple conversion rules
  208. const rules = [
  209. { regex: /<h3>(.*?)<\/h3>/g, replacement: '\n### $1\n' },
  210. { regex: /<h2>(.*?)<\/h2>/g, replacement: '\n## $1\n' },
  211. { regex: /<h1>(.*?)<\/h1>/g, replacement: '\n# $1\n' },
  212. { regex: /<p>(.*?)<\/p>/g, replacement: '$1\n' },
  213. { regex: /<ul>(.*?)<\/ul>/gs, replacement: '$1' },
  214. { regex: /<li>(.*?)<\/li>/g, replacement: '- $1' },
  215. { regex: /<pre.*?>(.*?)<\/pre>/gs, replacement: '\n\n``` \n$1\n```' },
  216. { regex: /<var>(.*?)<\/var>/g, replacement: '`$1`' },
  217. { regex: /<div.*?>(.*?)<\/div>/gs, replacement: '$1' },
  218. { regex: /<span.*?>(.*?)<\/span>/g, replacement: '$1' },
  219. { regex: /<section.*?>(.*?)<\/section>/gs, replacement: '$1' },
  220. { regex: /<hr>/g, replacement: '---' },
  221. { regex: /<br>/g, replacement: '\n' }
  222. ];
  223.  
  224. // Apply rules
  225. let markdown = html;
  226. rules.forEach(rule => {
  227. markdown = markdown.replace(rule.regex, rule.replacement);
  228. });
  229.  
  230. // Remove any remaining HTML tags
  231. markdown = markdown.replace(/<\/?[^>]+(>|$)/g, "");
  232.  
  233. return markdown.trim();
  234. }
  235.  
  236. // Function to convert HTML to Anki HTML
  237. function htmlToAnkiHtml(html) {
  238. // Simple conversion rules
  239. const rules = [
  240. { regex: /<h3>(.*?)<\/h3>/g, replacement: '\n<h3>$1</h3>\n' },
  241. { regex: /<h2>(.*?)<\/h2>/g, replacement: '\n<h2>$1</h2>\n' },
  242. { regex: /<h1>(.*?)<\/h1>/g, replacement: '\n<h1>$1</h1>\n' },
  243. { regex: /<p>(.*?)<\/p>/g, replacement: '<p>$1</p>\n' },
  244. { regex: /<ul>(.*?)<\/ul>/gs, replacement: '$1' },
  245. { regex: /<li>(.*?)<\/li>/g, replacement: '<li>$1</li>' },
  246. { regex: /<pre.*?>(.*?)<\/pre>/gs, replacement: '\n\n<pre>\n$1\n</pre>' },
  247. { regex: /<var>(.*?)<\/var>/g, replacement: '<var>$1</var>' },
  248. { regex: /<div.*?>(.*?)<\/div>/gs, replacement: '<div>$1</div>' },
  249. { regex: /<span.*?>(.*?)<\/span>/g, replacement: '<span><anki-mathjax>$1</anki-mathjax></span>' },
  250. { regex: /<section.*?>(.*?)<\/section>/gs, replacement: '<section>$1</section>' },
  251. { regex: /<hr>/g, replacement: '<hr>' },
  252. { regex: /<br>/g, replacement: '<br>' },
  253. { regex: /\$(.*?)\$/g, replacement: '$1' } // Convert TeX to Anki TeX
  254. ];
  255.  
  256. // Apply rules
  257. let ankiHtml = html;
  258. rules.forEach(rule => {
  259. ankiHtml = ankiHtml.replace(rule.regex, rule.replacement);
  260. });
  261.  
  262. return ankiHtml.trim();
  263. }
  264. })();