atcoder-refactor

Rewrites variable names in AtCoder problem statements.

As of 15.06.2020. See ბოლო ვერსია.

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 or Violentmonkey 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         atcoder-refactor
// @namespace    https://github.com/yoshrc/atcoder-refactor
// @version      1.0
// @description  Rewrites variable names in AtCoder problem statements.
// @author       yoshrc
// @match        https://atcoder.jp/contests/*/tasks/*
// @grant        none
// ==/UserScript==

// TODO
// - Inline edit like IDE's multiselection instead of popup
// - Use Roman font for multiple-character variable names
// - Remove variable name setting ... realized by reset variable name when set empty

(function() {
    'use strict';

    const ID_ATTR = 'data-atcoder-refactor-id';

    const storage = (() => {
        const STORAGE_KEY_PREFIX = 'atcoder-refactor-';
        const contest = location.href.match(/^https:\/\/atcoder\.jp\/contests\/([^/?]+)/)[1];
        const key = STORAGE_KEY_PREFIX + contest;

        return {
            load: () => {
                const idToNameStr = localStorage[key];
                if (!idToNameStr) {
                    return null;
                } else {
                    return JSON.parse(idToNameStr);
                }
            },

            save: idToName => {
                localStorage[key] = JSON.stringify(idToName);
            }
        };
    })();

    // When handlers called, the following setups must be completed:
    // - for all varaible elements, ID_ATTR attribute must be set
    // - id-name mapping should be set in the storage
    const handlers = (() => {
        const forEachVariable = (id, operationOnElement) => {
            document.querySelectorAll(`[${ID_ATTR}=${id}]`).forEach(elem => {
                operationOnElement(elem);
            });
        };

        return {
            onclick: varElem => {
                const idToName = storage.load();
                const id = varElem.getAttribute(ID_ATTR);
                const oldName = idToName[id];
                const newName = prompt('Set variable name', oldName);
                if (!newName || newName === '') {
                    return;
                }

                forEachVariable(id, elem => {
                    elem.textContent = newName;
                });
                idToName[id] = newName;
                storage.save(idToName);
            }
        };
    })();

    MathJax.Hub.Register.StartupHook('End', () => {
        const isVariable = mathJaxCharElem => mathJaxCharElem.textContent.match(/^[A-Za-z]+$/);
        const forEachVariable = operationOnElement => {
            document.querySelectorAll('.mjx-char').forEach(elem => {
                if (!isVariable(elem)) {
                    return;
                }
                operationOnElement(elem);
            });
        };

        const setupStorage = () => {
            const idToName = {};
            forEachVariable(elem => {
                const id = elem.textContent;
                idToName[id] = id;
            });
            storage.save(idToName);
        };

        // Storage data gets inconsistent if the problem statement is updated
        const fixInconsistentData = idToName => {
            let hasMissingId = false;
            const unnecessaryIds = new Set(Object.keys(idToName));
            forEachVariable(elem => {
                const id = elem.textContent;
                if (idToName[id]) {
                    unnecessaryIds.delete(id);
                } else {
                    idToName[id] = id;
                    hasMissingId = true;
                }
            });

            if (unnecessaryIds.size !== 0) {
                for (let id of unnecessaryIds) {
                    delete idToName[id];
                }
                return true;
            } else {
                return hasMissingId;
            }
        };

        const rewriteVariables = idToName => {
            forEachVariable(elem => {
                const id = elem.textContent;
                elem.textContent = idToName[id];
            });
        };

        forEachVariable(elem => {
            const id = elem.textContent;
            elem.setAttribute(ID_ATTR, id);
        });

        const idToName = storage.load();
        if (!idToName) {
            setupStorage();
        } else {
            if (fixInconsistentData(idToName)) {
                storage.save(idToName);
            }
            rewriteVariables(idToName);
        }

        forEachVariable(elem => {
            elem.onclick = () => handlers.onclick(elem);
        });
    });
})();