Codeforces: Export All Problems to PDF

Export all Codeforces contest problems to a printable PDF, styled like CPC contests.

  1. // ==UserScript==
  2. // @name Codeforces: Export All Problems to PDF
  3. // @namespace https://github.com/AnonMiraj
  4. // @author ezzeldin
  5. // @license GPL3
  6. // @description Export all Codeforces contest problems to a printable PDF, styled like CPC contests.
  7. // @match https://codeforces.com/group/*/contest/*
  8. // @match https://codeforces.com/gym/*
  9. // @match https://codeforces.com/contest/*
  10. // @grant none
  11. // @esversion 11
  12. // @version 1.0
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. if (!document.querySelector('table.datatable') && !document.querySelector('table.problems')) return;
  19.  
  20. const container = document.querySelector('div[style*="text-align: right"] a[href*="/problems"]')?.parentElement;
  21. if (!container) return;
  22. const originalHTML = document.body.innerHTML;
  23. const pdfLink = document.createElement('a');
  24. pdfLink.textContent = '💾 Save All as PDF';
  25. pdfLink.href = '#';
  26. pdfLink.style.marginLeft = '15px';
  27. pdfLink.style.color = '#0066cc';
  28. pdfLink.style.textDecoration = 'none';
  29. pdfLink.style.cursor = 'pointer';
  30. container.appendChild(pdfLink);
  31.  
  32. pdfLink.addEventListener('click', async (e) => {
  33. e.preventDefault();
  34. await exportPDF(pdfLink);
  35. });
  36.  
  37. async function exportPDF(link) {
  38. const originalText = link.textContent;
  39. link.style.pointerEvents = 'none';
  40. link.textContent = 'Fetching…';
  41.  
  42. const selectors = [
  43. 'table.datatable tr td.id a',
  44. 'table.datatable tr td.index a',
  45. 'table.problems tr td.id a',
  46. 'table.problems tr td.index a',
  47. 'table.datatable tr a[href*="/problem/"]',
  48. 'table.problems tr a[href*="/problem/"]'
  49. ];
  50.  
  51. const anchors = Array.from(document.querySelectorAll(selectors.join(',')));
  52. const seen = new Set(), links = [];
  53. anchors.forEach(a => {
  54. if (!seen.has(a.href)) {
  55. seen.add(a.href);
  56. links.push(a.href);
  57. }
  58. });
  59.  
  60. if (!links.length) return alert('No problem links found!');
  61.  
  62. const problemsHTML = await Promise.all(
  63. links.map(url => fetchProblem(url))
  64. );
  65.  
  66. document.body.innerHTML = problemsHTML
  67. .map((html, i) => html || `<div><h2>Problem ${links[i].split('/').pop()} failed to load.</h2></div>`);
  68.  
  69. document.querySelectorAll('.test-example-line-even, .test-example-line-odd').forEach(el => {
  70. el.classList.remove('test-example-line-even', 'test-example-line-odd');
  71. });
  72.  
  73. document.querySelectorAll('.problem-statement').forEach((prob, i, arr) => {
  74. const inputs = prob.querySelectorAll('.input');
  75. const outputs = prob.querySelectorAll('.output');
  76. if (inputs.length && outputs.length) {
  77. const table = document.createElement('table');
  78. table.className = 'samples-table';
  79. table.style.width = '100%';
  80. table.style.borderCollapse = 'collapse';
  81. table.style.marginBottom = '20px';
  82.  
  83. const header = table.insertRow();
  84. ['Standard Input', 'Standard Output'].forEach(text => {
  85. const th = document.createElement('th');
  86. th.textContent = text;
  87. th.style.border = '1px solid black';
  88. th.style.padding = '8px';
  89. th.style.textAlign = 'center';
  90. th.style.width = '50%';
  91. header.appendChild(th);
  92. });
  93.  
  94. for (let j = 0; j < Math.min(inputs.length, outputs.length); j++) {
  95. const row = table.insertRow();
  96. [inputs[j], outputs[j]].forEach(el => {
  97. const cell = row.insertCell();
  98. cell.style.border = '1px solid black';
  99. cell.style.padding = '8px';
  100. cell.style.verticalAlign = 'top';
  101. cell.appendChild(el);
  102. });
  103. }
  104.  
  105. const sample = prob.querySelector('.sample-tests') || prob.querySelector('.sample-test') || prob;
  106. sample?.parentNode?.insertBefore(table, sample.nextSibling);
  107. prob.querySelector('.sample-tests')?.remove();
  108. prob.querySelector('.sample-test')?.remove();
  109. }
  110.  
  111. if (i < arr.length - 1) {
  112. const pageBreak = document.createElement('div');
  113. pageBreak.style.pageBreakBefore = 'always';
  114. prob.parentNode.insertBefore(pageBreak, prob.nextSibling);
  115. }
  116. });
  117.  
  118. const style = document.createElement('style');
  119. style.textContent = `
  120. .samples-table th, .samples-table td {
  121. border: 1px solid black;
  122. padding: 8px;
  123. vertical-align: top;
  124. text-align: left;
  125. }
  126. .samples-table pre {
  127. white-space: pre-wrap;
  128. word-wrap: break-word;
  129. font-size: 12px;
  130. margin: 0;
  131. }
  132. @media print {
  133. .samples-table th, .samples-table td {
  134. border: 1px solid black;
  135. padding: 8px;
  136. }
  137. }
  138. `;
  139. document.head.appendChild(style);
  140.  
  141. ['#MathJax_Message', '.status-bar', '#status', '.print-hide']
  142. .forEach(sel => document.querySelectorAll(sel).forEach(el => el.style.display = 'none'));
  143.  
  144. const finalize = () => setTimeout(() => {
  145. document.getElementById('MathJax_Message')?.style.setProperty('display', 'none', 'important');
  146. window.print();
  147.  
  148. setTimeout(() => {
  149. document.body.innerHTML = originalHTML;
  150. link.style.pointerEvents = '';
  151. link.textContent = originalText;
  152. }, 500);
  153. }, 500);
  154.  
  155. if (window.MathJax) {
  156. try {
  157. if (MathJax.typesetPromise) {
  158. await MathJax.typesetPromise([document.body]);
  159. finalize();
  160. } else if (MathJax.Hub?.Queue) {
  161. MathJax.Hub.Queue(["Typeset", MathJax.Hub, document.body], finalize);
  162. } else finalize();
  163. } catch (e) {
  164. finalize();
  165. }
  166. } else finalize();
  167.  
  168. link.style.pointerEvents = '';
  169. link.textContent = originalText;
  170. }
  171.  
  172. async function fetchProblem(url, retries = 2) {
  173. try {
  174. const res = await fetch(url);
  175. const txt = await res.text();
  176. const doc = new DOMParser().parseFromString(txt, 'text/html');
  177. const prob = doc.querySelector('.problem-statement');
  178. if (!prob) throw new Error('Missing problem-statement');
  179.  
  180. prob.querySelectorAll('.input-output-copier').forEach(el => el.remove());
  181.  
  182. const header = doc.querySelector('.header');
  183. let title = 'Unknown Problem';
  184. let inputFile = 'standard input';
  185. let outputFile = 'standard output';
  186. let timeLimit = 'N/A';
  187. let memoryLimit = 'N/A';
  188.  
  189. if (header) {
  190. title = header.querySelector('.title')?.textContent.trim() || title;
  191. inputFile = header.querySelector('.input-file')?.textContent.replace('input', '').trim() || inputFile;
  192. outputFile = header.querySelector('.output-file')?.textContent.replace('output', '').trim() || outputFile;
  193. timeLimit = header.querySelector('.time-limit')?.textContent.replace('time limit per test', '').trim() || timeLimit;
  194. memoryLimit = header.querySelector('.memory-limit')?.textContent.replace('memory limit per test', '').trim() || memoryLimit;
  195. header.remove();
  196. }
  197.  
  198. const h2 = document.createElement('h2');
  199. h2.textContent = title;
  200. h2.style.marginBottom = '0.2em';
  201. h2.style.textAlign = 'left';
  202. prob.insertBefore(h2, prob.firstChild);
  203.  
  204. const metaDiv = document.createElement('div');
  205. metaDiv.className = 'problem-metadata';
  206. metaDiv.style.marginBottom = '1em';
  207. metaDiv.innerHTML = `
  208. <pre style="font-family: monospace; margin-left: 20px; font-size: 14px;">
  209. Input file:\t${inputFile}
  210. Output file:\t${outputFile}
  211. Time limit:\t${timeLimit}
  212. Memory limit:\t${memoryLimit}
  213. </pre>
  214. `;
  215.  
  216.  
  217. prob.insertBefore(metaDiv, prob.firstChild.nextSibling);
  218.  
  219. prob.querySelectorAll('.input .title, .output .title').forEach(el => el.remove());
  220. return prob.outerHTML;
  221.  
  222. } catch (err) {
  223. if (retries > 0) {
  224. await new Promise(r => setTimeout(r, 400));
  225. return fetchProblem(url, retries - 1);
  226. }
  227. return null;
  228. }
  229. }
  230. })();
  231.  
  232.