spoiled competitive programming

Parse AtCoder problem statement into sections and create Anki CSV

Fra og med 08.04.2025. Se den nyeste version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         spoiled competitive programming
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Parse AtCoder problem statement into sections and create Anki CSV
// @author       Harui (totally helped by GPT-4o)
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Create buttons for Japanese
    const openMarkdownButtonJa = document.createElement('button');
    openMarkdownButtonJa.textContent = '新しいタブで日本語のMarkdownを開く';
    openMarkdownButtonJa.style.position = 'fixed';
    openMarkdownButtonJa.style.left = '10px';
    openMarkdownButtonJa.style.bottom = '145px';
    openMarkdownButtonJa.style.zIndex = '1000';
    openMarkdownButtonJa.style.padding = '10px';
    openMarkdownButtonJa.style.backgroundColor = '#4CAF50';
    openMarkdownButtonJa.style.color = 'white';
    openMarkdownButtonJa.style.border = 'none';
    openMarkdownButtonJa.style.borderRadius = '5px';
    openMarkdownButtonJa.style.cursor = 'pointer';

    const openHtmlButtonJa = document.createElement('button');
    openHtmlButtonJa.textContent = '新しいタブで日本語のHTMLを開く';
    openHtmlButtonJa.style.position = 'fixed';
    openHtmlButtonJa.style.left = '10px';
    openHtmlButtonJa.style.bottom = '100px';
    openHtmlButtonJa.style.zIndex = '1000';
    openHtmlButtonJa.style.padding = '10px';
    openHtmlButtonJa.style.backgroundColor = '#4CAF50';
    openHtmlButtonJa.style.color = 'white';
    openHtmlButtonJa.style.border = 'none';
    openHtmlButtonJa.style.borderRadius = '5px';
    openHtmlButtonJa.style.cursor = 'pointer';

    // Append the buttons to the body
    document.body.appendChild(openMarkdownButtonJa);
    document.body.appendChild(openHtmlButtonJa);

    // Event listener for open Markdown button (Japanese)
    openMarkdownButtonJa.addEventListener('click', () => {
        const markdownContent = htmlToMarkdown(extractHtml('ja'));

        // Open the Markdown content in a new tab
        const newTab = window.open();
        newTab.document.open();
        newTab.document.write('<pre>' + markdownContent + '</pre>');
        newTab.document.close();
    });

    // Event listener for open HTML button (Japanese)
    openHtmlButtonJa.addEventListener('click', () => {
        const htmlContent = htmlToAnkiHtml(extractHtml('ja'));

        // Open the HTML content in a new tab
        const newTab = window.open();
        newTab.document.open();
        newTab.document.write(htmlContent);
        newTab.document.close();
    });

    // Create buttons and style them
    const openMarkdownButtonEn = document.createElement('button');
    openMarkdownButtonEn.textContent = 'Open English Markdown in New Tab';
    openMarkdownButtonEn.style.position = 'fixed';
    openMarkdownButtonEn.style.left = '10px';
    openMarkdownButtonEn.style.bottom = '55px';
    openMarkdownButtonEn.style.zIndex = '1000';
    openMarkdownButtonEn.style.padding = '10px';
    openMarkdownButtonEn.style.backgroundColor = '#4CAF50';
    openMarkdownButtonEn.style.color = 'white';
    openMarkdownButtonEn.style.border = 'none';
    openMarkdownButtonEn.style.borderRadius = '5px';
    openMarkdownButtonEn.style.cursor = 'pointer';

    const openHtmlButtonEn = document.createElement('button');
    openHtmlButtonEn.textContent = 'Open English HTML in New Tab';
    openHtmlButtonEn.style.position = 'fixed';
    openHtmlButtonEn.style.left = '10px';
    openHtmlButtonEn.style.bottom = '10px';
    openHtmlButtonEn.style.zIndex = '1000';
    openHtmlButtonEn.style.padding = '10px';
    openHtmlButtonEn.style.backgroundColor = '#4CAF50';
    openHtmlButtonEn.style.color = 'white';
    openHtmlButtonEn.style.border = 'none';
    openHtmlButtonEn.style.borderRadius = '5px';
    openHtmlButtonEn.style.cursor = 'pointer';

    // Append the buttons to the body
    document.body.appendChild(openMarkdownButtonEn);
    document.body.appendChild(openHtmlButtonEn);

    // Create button for Anki CSV download
    const downloadCsvButton = document.createElement('button');
    downloadCsvButton.textContent = 'Anki用CSVをダウンロード';
    downloadCsvButton.style.position = 'fixed';
    downloadCsvButton.style.left = '10px';
    downloadCsvButton.style.bottom = '190px';
    downloadCsvButton.style.zIndex = '1000';
    downloadCsvButton.style.padding = '10px';
    downloadCsvButton.style.backgroundColor = '#4CAF50';
    downloadCsvButton.style.color = 'white';
    downloadCsvButton.style.border = 'none';
    downloadCsvButton.style.borderRadius = '5px';
    downloadCsvButton.style.cursor = 'pointer';

    // Append the button to the body
    document.body.appendChild(downloadCsvButton);

    // Event listener for download CSV button
    downloadCsvButton.addEventListener('click', () => {
        const htmlContent = htmlToAnkiHtml(extractHtml('ja'));
        const csvContent = generateAnkiCsv(htmlContent);

        // Create a blob from the CSV content
        const blob = new Blob([csvContent], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);

        // Create a link element and trigger the download
        const a = document.createElement('a');
        a.href = url;
        a.download = 'anki.csv';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    });

    // Function to extract parts and remove Katex, returning HTML
    function extractHtml(lang) {
        const parts = document.querySelectorAll(`span.lang-${lang}`);
        const problemTitle = document.title;

        let htmlContent = `<h1>${problemTitle}</h1>\n\n`;

        parts.forEach(part => {
            // Clone the part to avoid modifying the original document
            const clone = part.cloneNode(true);

            // Remove "Copy" buttons
            const copyButtons = clone.querySelectorAll('.btn-copy, .btn-pre, .div-btn-copy');
            copyButtons.forEach(button => button.remove());

            // Remove Katex elements
            const katexElements = clone.querySelectorAll('.katex');
            katexElements.forEach(katex => {
                const tex = katex.querySelector('.katex-mathml annotation');
                if (tex) {
                    const textNode = document.createTextNode(tex.textContent);
                    katex.parentNode.replaceChild(textNode, katex);
                }
            });

            // Append HTML content
            htmlContent += clone.outerHTML + '\n\n';
        });

        return htmlContent;
    }

    // Function to generate Anki CSV
    function generateAnkiCsv(htmlContent) {
        const problemId = window.location.pathname.split('/')[2];
        const problemAlpha = window.location.pathname.split('/')[4].split('_')[1];
        const problemTitle = document.querySelector('title').textContent.trim();
        const problemName = `${problemId.toUpperCase()}_${problemAlpha.toUpperCase()} ${problemTitle}`;
        const problemUrl = window.location.href;

        // Replace double quotes with double double quotes
        const escapedHtmlContent = htmlContent.replace(/"/g, '""');

        const csvContent = `#separator:tab\n#html:true\n"${problemName}"\t"<a href=""${problemUrl}"">${problemUrl}</a>"\t"${escapedHtmlContent}"`;
        return csvContent;
    }


    // Event listener for open Markdown button (English)
    openMarkdownButtonEn.addEventListener('click', () => {
        const markdownContent = htmlToMarkdown(extractHtml('en'));

        // Open the Markdown content in a new tab
        const newTab = window.open();
        newTab.document.open();
        newTab.document.write('<pre>' + markdownContent + '</pre>');
        newTab.document.close();
    });

    // Event listener for open HTML button (English)
    openHtmlButtonEn.addEventListener('click', () => {
        const htmlContent = extractHtml('en');

        // Open the HTML content in a new tab
        const newTab = window.open();
        newTab.document.open();
        newTab.document.write(htmlContent);
        newTab.document.close();
    });

    // Function to convert HTML to Markdown
    function htmlToMarkdown(html) {
        // Simple conversion rules
        const rules = [
            { regex: /<h3>(.*?)<\/h3>/g, replacement: '\n### $1\n' },
            { regex: /<h2>(.*?)<\/h2>/g, replacement: '\n## $1\n' },
            { regex: /<h1>(.*?)<\/h1>/g, replacement: '\n# $1\n' },
            { regex: /<p>(.*?)<\/p>/g, replacement: '$1\n' },
            { regex: /<ul>(.*?)<\/ul>/gs, replacement: '$1' },
            { regex: /<li>(.*?)<\/li>/g, replacement: '- $1' },
            { regex: /<pre.*?>(.*?)<\/pre>/gs, replacement: '\n\n``` \n$1\n```' },
            { regex: /<var>(.*?)<\/var>/g, replacement: '`$1`' },
            { regex: /<div.*?>(.*?)<\/div>/gs, replacement: '$1' },
            { regex: /<span.*?>(.*?)<\/span>/g, replacement: '$1' },
            { regex: /<section.*?>(.*?)<\/section>/gs, replacement: '$1' },
            { regex: /<hr>/g, replacement: '---' },
            { regex: /<br>/g, replacement: '\n' }
        ];

        // Apply rules
        let markdown = html;
        rules.forEach(rule => {
            markdown = markdown.replace(rule.regex, rule.replacement);
        });

        // Remove any remaining HTML tags
        markdown = markdown.replace(/<\/?[^>]+(>|$)/g, "");

        return markdown.trim();
    }

    // Function to convert HTML to Anki HTML
    function htmlToAnkiHtml(html) {
        // Simple conversion rules
        const rules = [
            { regex: /<h3>(.*?)<\/h3>/g, replacement: '\n<h3>$1</h3>\n' },
            { regex: /<h2>(.*?)<\/h2>/g, replacement: '\n<h2>$1</h2>\n' },
            { regex: /<h1>(.*?)<\/h1>/g, replacement: '\n<h1>$1</h1>\n' },
            { regex: /<p>(.*?)<\/p>/g, replacement: '<p>$1</p>\n' },
            { regex: /<ul>(.*?)<\/ul>/gs, replacement: '$1' },
            { regex: /<li>(.*?)<\/li>/g, replacement: '<li>$1</li>' },
            { regex: /<pre.*?>(.*?)<\/pre>/gs, replacement: '\n\n<pre>\n$1\n</pre>' },
            { regex: /<var>(.*?)<\/var>/g, replacement: '<var>$1</var>' },
            { regex: /<div.*?>(.*?)<\/div>/gs, replacement: '<div>$1</div>' },
            { regex: /<span.*?>(.*?)<\/span>/g, replacement: '<span><anki-mathjax>$1</anki-mathjax></span>' },
            { regex: /<section.*?>(.*?)<\/section>/gs, replacement: '<section>$1</section>' },
            { regex: /<hr>/g, replacement: '<hr>' },
            { regex: /<br>/g, replacement: '<br>' },
            { regex: /\$(.*?)\$/g, replacement: '$1' } // Convert TeX to Anki TeX
        ];

        // Apply rules
        let ankiHtml = html;
        rules.forEach(rule => {
            ankiHtml = ankiHtml.replace(rule.regex, rule.replacement);
        });

        return ankiHtml.trim();
    }
})();