洛谷 - 梦回考场

洛谷题目页面改为NOI/NOIP/CSP经典PDF风格,享受沉浸(致郁)式刷题。

当前为 2025-07-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         洛谷 - 梦回考场
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  洛谷题目页面改为NOI/NOIP/CSP经典PDF风格,享受沉浸(致郁)式刷题。
// @author       Jerrycyx (Luogu UID 545986)
// @match        https://www.luogu.com.cn/problem/*
// @license      MPL-2.0
// @run-at       document-idle
// @grant        GM_addStyle
// @require      https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U=
// ==/UserScript==

//TODO: More SimSun
//TODO: Translation
(function () {
    "use strict";

    const path = window.location.pathname;
    const validPrefixes = ["P", "B", "CF", "SP", "AT", "UVA", "T", "U"];
    const regex = /^\/problem\/([^/]+)$/;
    const match = path.match(regex);
    if (!match) return;
    if (!validPrefixes.some(prefix => match[1].startsWith(prefix))) return;

    function processElements(container, selector, processAll, processor) {
        const elements = container.querySelectorAll(selector);
        if (elements.length === 0) {
            console.info(`[Luogu-RER] No elements found with selector: "${selector}"`);
            return false;
        }
        if (processAll) {
            elements.forEach(el => processor(el));
        }
        else {
            processor(elements[0]);
        }
        return true;
    }
    function removeFirst(container, selector) { processElements(container, selector, false, e => { e.remove(); }); }
    function removeAll(container, selector) { processElements(container, selector, true, e => { e.remove(); }); }

    window.addEventListener("load", function () {
        console.log("[Luogu-RER] Loading...");
        /* === Defination === */

        const mainElement = document.querySelector("#app > .main-container");
        mainElement.id = "Main-element";
        const oringinalProblemHeader = mainElement.querySelector(".theme-bg > .theme-fg > .columba-content-wrap.header-layout");
        const titleContent = oringinalProblemHeader.querySelector("h1.lfe-h1").textContent;
        const timeLimit = oringinalProblemHeader.querySelector(".stat > .field:nth-child(3) > .stat-text.value").textContent;
        const memoryLimit = oringinalProblemHeader.querySelector(".stat > .field:nth-child(4) > .stat-text.value").textContent;
        const problemElement = mainElement.querySelector("main .columba-content-wrap > .sidebar-container > .main > .problem");
        problemElement.id = "Problem-element";

        GM_addStyle(`
            #Problem-element {
                --common-font-family: KaTeX_Main, SimSun;
                --strong-font-family: KaTeX_Main, SImHei;
            }
            .custom-common-text {
                font-family: var(--common-font-family);
                font-weight: normal;
            }
            .custom-strong-text {
                font-family: var(--strong-font-family);
                font-weight: normal;
            }
        `);

        elmGetter.each("#Problem-element p, #Problem-element ul, #Problem-element ol, #Problem-element .attachments", problemElement, e => {
            e.classList.add("custom-common-text");
        });

        /* === Removement === */

        removeAll(document, "#app > nav");
        removeAll(document, "#app > .top-bar");
        removeAll(document, "#app > .dropdown");
        removeAll(document, "#app > .modal.hide");
        removeAll(document, "#app > .page-loading");
        removeAll(document, "#app > .nav-scrollbar");
        removeAll(mainElement, ".theme-bg");
        removeAll(mainElement, "main .columba-content-wrap > .sidebar-container > .side");
        removeFirst(problemElement, ".problem-block-actions");
        removeAll(problemElement, ".io-sample > .io-sample-block button.lform-size-small");

        elmGetter.each("p", problemElement, e => {
            function isValidChainImage(e) {
                if (e.tagName === 'IMG') return true;
                let current = e;
                while (current) {
                    const childNodes = current.childNodes;
                    if (childNodes.length !== 1 || childNodes[0].nodeType !== 1) {
                        return false;
                    }
                    current = current.firstElementChild;
                    if (current && current.tagName === 'IMG') {
                        return true;
                    }
                }
            };
            if(isValidChainImage(e)) e.classList.add("single-line-image-p");
        });

        GM_addStyle(`
            #Main-element {
                margin: 46px 0 12px 0;
                width: auto;
            }
            #Main-element > main {
                background-color: unset;
            }
            @media (prefers-color-scheme: light) {
                body {
                    background-color: #e6e6e6;
                }
            }
            @media (prefers-color-scheme: dark) {
                body {
                    background-color: #333333;
                }
            }
            #Main-element > main > .columba-content-wrap {
                padding: 0px;
            }
            #Main-element > main > .columba-content-wrap > .sidebar-container {
                margin: unset;
            }
            #Problem-element {
                color: black;
                width: 1191px;
                margin: 0 auto;
                padding: 77px 141px 156px 141px;
                border-radius: 0px;
                box-shadow: 0 1px 10px 0px #1a1a1a1a;
            }

            #Problem-element p:not(.single-line-image-p) {
                text-indent: 2em;
            }
            #Problem-element p, #Problem-element ul, #Problem-element ol, #Problem-element .attachments {
                font-size: 24px;
                line-height: 1.6;
                margin: unset;
            }
            #Problem-element ul, #Problem-element ol {
                margin-left: 1em;
            }
            #Problem-element :is(ul, ol) :is(ul, ol) {
                margin-left: unset;
            }
            #Problem-element :is(ul, ol) :is(ul, ol) li::marker {
                content: "–  ";
            }

            .katex:not(.katex-display .katex) {
                font-size: 1em;
            }
            
            #Problem-element ::selection{
                background-color: #99c1da; //TODO
                mix-blend-mode: multiply;
                color: inherit;
            }

            .single-line-image-p {
                text-align: center;
            }
        `);

        /* === Toolbar === */

        const customToolbar = document.createElement("div");
        customToolbar.classList.add("custom-toolbar");

        const customToolbarLeft = document.createElement("div");
        customToolbarLeft.classList.add("custom-toolbar-left");

        const customToolbarCenter = document.createElement("div");
        customToolbarCenter.classList.add("custom-toolbar-center");

        const customToolbarRight = document.createElement("div");
        customToolbarRight.classList.add("custom-toolbar-right");

        customToolbar.append(customToolbarLeft, customToolbarCenter, customToolbarRight);
        mainElement.insertBefore(customToolbar, mainElement.firstChild);

        const customToolbarSubmit = document.createElement("a");
        customToolbarSubmit.href = "#submit";
        customToolbarSubmit.textContent = "提交答案";
        customToolbarSubmit.classList.add("custom-toolbar-submit", "custom-toolbar-button");
        customToolbarLeft.appendChild(customToolbarSubmit);

        GM_addStyle(`
            @media (prefers-color-scheme: light) {
                .custom-toolbar {
                    --toolbar-bg: #f7f7f7;
                    --toolbar-border: #bebebe;
                    --toolbar-text: #262626;
                    --toolbar-button-hover: #dddddd;
                    --toolbar-button-active: #efefef;
                    --toolbar-separator-color: #b6b6b6;
                }
            }
            @media (prefers-color-scheme: dark) {
                .custom-toolbar {
                    --toolbar-bg: #3b3b3b;
                    --toolbar-border: #4f4f4f;
                    --toolbar-text: #ffffff;
                    --toolbar-button-hover: #545454;
                    --toolbar-button-active: #424242;
                    --toolbar-separator-color: #737373;
                }
            }

            .custom-toolbar {
                background-color: var(--toolbar-bg);
                border-bottom: 1px solid var(--toolbar-border);
                color: var(--toolbar-text);

                z-index: 10;
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 41px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                box-sizing: border-box;
            }

            .custom-toolbar-left, .custom-toolbar-center, .custom-toolbar-right {
                height: 100%;
                display: flex;
                align-items: center;
            }

            .custom-toolbar-left {
                justify-content: flex-start; 
                margin-right: auto;
            }

            .custom-toolbar-center {
                justify-content: center;
                margin-left: auto;
                margin-right: auto;
            }

            .custom-toolbar-right {
                justify-content: flex-end;
                margin-left: auto;
            }

            .custom-toolbar-button {
                cursor: default;
                border: 2px solid transparent;
                border-radius: 2px;
                padding: 0 8px;
                margin: 4px 0;
                height: 28px;
                line-height: 28px;
                text-align: center;
                transition: all 0.1s ease-in-out;
                color: var(--toolbar-text);
                outline: none;
            }
            .custom-toolbar-button:hover {
                background-color: var(--toolbar-button-hover);
                color: inherit;
                filter: unset;
            }
            .custom-toolbar-button:active {
                background-color: var(--toolbar-button-active);
                color: inherit;
            }

            .custom-toolbar-separator {
                margin: 0 4px;
                width: 1px;
                height: 16px;
                background-color: var(--toolbar-separator-color);
            }
        `)

        /* === Stat === */

        const customStat = document.createElement("div");
        customStat.classList.add("custom-stat", "custom-common-text");

        const customTimeStat = document.createElement("div");
        customTimeStat.classList.add("custom-time-stat");
        customTimeStat.textContent = "时间限制:" + timeLimit;

        const customMemoryStat = document.createElement("div");
        customMemoryStat.classList.add("custom-memory-stat");
        customMemoryStat.textContent = "空间限制:" + memoryLimit;

        customStat.append(customTimeStat, customMemoryStat);
        problemElement.insertBefore(customStat, problemElement.firstChild);

        GM_addStyle(`
            .custom-stat {
                font-size: 24px;
                display: flex;
                flex-wrap: wrap;
                justify-content: center;
            }
            .custom-time-stat, .custom-memory-stat {
                margin-left: 1em;
                margin-right: 1em;
            }
        `);

        /* === Header === */

        const customTitleElement = document.createElement("h1");
        customTitleElement.classList.add("lfe-h1", "custom-title");
        customTitleElement.textContent = titleContent.substring(titleContent.indexOf(" ") + 1);
        problemElement.insertBefore(customTitleElement, customStat);

        const customHeaderElement = document.createElement("header");
        customHeaderElement.classList.add("custom-header", "custom-common-text");
        problemElement.insertBefore(customHeaderElement, customTitleElement);

        const customHeaderLeftTextElement = document.createElement("div");
        customHeaderLeftTextElement.classList.add("custom-header-left-text");
        customHeaderLeftTextElement.textContent = "洛谷 Luogu";
        customHeaderElement.appendChild(customHeaderLeftTextElement);

        const customHeaderRightTextElement = document.createElement("div");
        customHeaderRightTextElement.classList.add("custom-header-right-text");
        customHeaderRightTextElement.textContent = titleContent;
        customHeaderElement.appendChild(customHeaderRightTextElement);

        const customHeaderHrElement = document.createElement("hr");
        customHeaderHrElement.classList.add("custom-header-hr");
        customHeaderElement.appendChild(customHeaderHrElement);

        GM_addStyle(`
            .custom-header {
                font-size: 20px;
                display: flex;
                flex-wrap: wrap;
                margin: 0px 0px 48px 0px;
            }
            .custom-header-left-text, .custom-header-right-text {
                margin: 0px;
            }
            .custom-header-left-text {
                margin-right: auto;
            }
            .custom-header-hr {
                flex-basis: 100%;
                border: 1px solid #bfbfbf;
                margin: 0px;
            }
        `)

        /* === Sample Blocks === */

        processElements(problemElement, "h2.lfe-h2", true, h2 => {
            if (h2.textContent.trim() === "输入输出样例") { h2.remove(); }
        });

        processElements(problemElement, ".io-sample > .io-sample-block b", true, b => {
            const content = b.textContent.trim();
            const regex = /^(输入|输出)\s*#(\d+)$/;
            const match = content.match(regex);
            if (match) {
                const type = match[1];
                const num = match[2];
                const h2 = document.createElement("h2");
                h2.classList.add("lfe-h2");
                h2.textContent = `样例 ${num} ${type}`;
                b.parentNode.parentNode.replaceChild(h2, b.parentNode);

            }
        });

        /* === Titles (h1 ~ h6) === */

        elmGetter.each("h1, h2, h3, h4, h5, h6", problemElement, h => {
            h.classList.add("H123456", "custom-strong-text");
            if (!h.classList.contains("custom-title")) {
                if (h.textContent[0] !== "【") { h.textContent = `【${h.textContent}` };
                if (h.textContent[h.textContent.length - 1] !== "】") { h.textContent = `${h.textContent}】` };
            }
        });

        GM_addStyle(`
            .custom-title {
                text-align: center;
            }
            .H123456 {
                font-weight: normal;
            }
            #Problem-element .H123456:not(.custom-title) {
                text-indent: 1em;
            }
            #Problem-element h1 {
                font-size: 36px;
                margin: 37px 0px;
            }
            #Problem-element h2 {
                margin-top: 43px;
                margin-bottom: 18px;
                font-size: 26px;
            }
            #Problem-element h3 {
                margin-top: 24px;
                margin-bottom: 7px;
                font-size: 25px;
            }
            #Problem-element h4, #Problem-element h5, #Problem-element h6 {
                margin-top: 10px;
                margin-bottom: 7px;
                font-size: 24px;
            }
        `)

        /* === Dots under <strong> === */

        elmGetter.each("strong", problemElement, strong => {
            strong.classList.add("custom-strong-text");
            const fragment = document.createDocumentFragment();
            const processNode = (node) => {
                if (node.nodeType === Node.TEXT_NODE) {
                    const text = node.textContent;
                    const segments = text.split(/([\u4e00-\u9fa5])/);
                    segments.forEach(segment => {
                        if (!segment) return;

                        if (/^[\u4e00-\u9fa5]$/.test(segment)) {
                            const span = document.createElement("span");
                            span.classList.add("custom-hanzi-dot");
                            span.textContent = segment;

                            const svgNS = "http://www.w3.org/2000/svg";
                            const svg = document.createElementNS(svgNS, "svg");
                            svg.setAttribute("class", "custom-dot");
                            svg.setAttribute("viewBox", "0 0 10 10");

                            const circle = document.createElementNS(svgNS, "circle");
                            circle.setAttribute("cx", "5");
                            circle.setAttribute("cy", "5");
                            circle.setAttribute("r", "3");
                            circle.setAttribute("fill", "black");

                            svg.appendChild(circle);
                            span.appendChild(svg);
                            fragment.appendChild(span);
                        } else {
                            fragment.appendChild(document.createTextNode(segment));
                        }
                    });
                } else if (node.nodeType === Node.ELEMENT_NODE) {
                    fragment.appendChild(node.cloneNode(true));
                }
            };
            strong.childNodes.forEach(node => processNode(node));
            strong.innerHTML = "";
            strong.appendChild(fragment);
        });

        GM_addStyle(`
            .custom-hanzi-dot {
                position: relative;
            }
            .custom-hanzi-dot svg.custom-dot {
                height: 0.15em;
                position: absolute;
                left: 0.5em;
                bottom: -0.1em;
                transform: translateX(-50%);
                pointer-events: none;
            }
        `)

        /* === Code Blocks === */

        elmGetter.each("pre", problemElement, pre => {
            const code = pre.querySelector("code") || pre;
            const text = code.textContent;
            const lines = text.split("\n");

            let lastNonEmptyIndex = -1;
            for (let i = lines.length - 1; i >= 0; i--) {
                if (lines[i].trim() !== "") {
                    lastNonEmptyIndex = i;
                    break;
                }
            }

            const nonEmptyLines = lastNonEmptyIndex >= 0
                ? lines.slice(0, lastNonEmptyIndex + 1)
                : [];
            const lineCount = nonEmptyLines.length;

            const lineNumbersContainer = document.createElement("div");
            lineNumbersContainer.classList.add("custom-pre-line-number");

            for (let i = 1; i <= lineCount; i++) {
                const lineNumber = document.createElement("div");
                lineNumber.textContent = i;
                lineNumbersContainer.appendChild(lineNumber);
            }

            const digits = lineCount.toString().length;
            lineNumbersContainer.style.width = `${digits * 0.5 + 0.5}em`;

            const wrapper = document.createElement("div");
            wrapper.classList.add("custom-pre-wrapper");

            pre.parentNode.insertBefore(wrapper, pre);
            wrapper.appendChild(lineNumbersContainer);
            wrapper.appendChild(pre);
        });

        removeAll(problemElement, ".code-container > button.copy-button");

        GM_addStyle(`
            #Problem-element > .lfe-marked-wrap {
                overflow: visible;
            }

            #Problem-element > .io-sample {
                margin: 0px;
                display: block;
            }
            #Problem-element > .io-sample > .io-sample-block {
                margin: 0px;
            }

            #Problem-element pre, #Problem-element code, .custom-pre-line-number {
                font-family: Consolas;
                background-color: unset;
            }
            #Problem-element pre, #Problem-element code {
                font-size: 24px;
                line-height: 1.5;
            }

            #Problem-element .custom-pre-line-number {
                position: absolute;
                text-align: right;
                padding: 6px;
                right: 100%;
                font-size: 20px;
                color: #949494;
                top: 5px;
            }
            #Problem-element .custom-pre-line-number > div {
                position: relative;
                height: 36px;
                top: 4px;
            }

            #Problem-element code {
                border: none;
            }
            #Problem-element pre {
                border: solid 1px blue;
                border-radius: 0;
                padding: 5px;
            }

            #Problem-element .custom-pre-wrapper {
                position: relative;
                margin: 27px -5px 62px 17px;
            }
        `)

        /* === Links, Focus and Tables === */

        elmGetter.each("a", problemElement, e => {
            e.classList.add("custom-strong-text");
        });
        elmGetter.each("table > thead > tr > th, table > tbody > tr > td", problemElement, e => {
            e.classList.add("custom-common-text");
        });

        GM_addStyle(`
            #Problem-element a {
                color: black;
                font-style: italic;
                transition: unset;
            }
            #Problem-element a:hover {
                filter: none;
            }
            #Problem-element :focus-visible {
                outline: 3px dashed black;
            }

            #Problem-element table {
                display: table;
                margin: 25px auto;
                width: fit-content;
            }

            #Problem-element table > thead > tr {
                border-bottom: 3px solid black;
            }
            #Problem-element table > thead > tr > th, #Problem-element table > tbody > tr > td {
                font-size: 24px;
                border: 1px solid black;
            }
            #Problem-element table {
                border-top: 4px solid black;
                border-bottom: 4px solid black;
                border-left: 2px solid white;
                border-right: 2px solid white;
            }
        `)

        console.log("[Luogu-RER] Loaded!");
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址