SYZOJ ranklist 溢出修复

前言

这个系列怎么更新这么多期了?感兴趣的读者可以去阅读前面的文章。

正文

在 SYZOJ,当一场比赛的题目过多时,ranklist 展示界面表格会溢出,且不能滚动看到右侧题目的得分情况,必须手动缩放整个页面,十分麻烦,不友好。

我们可以简单调整以下做到如下效果,打开页面自动缩放:

当然可以点击按钮放大至无缩放查看:

当然如果不缩放不会溢出,那么默认无缩放。

代码

css 没有变更,可以参考上一篇文章

JS 代码
// ==UserScript==
// @name         SYZOJ darker
// @namespace    http://tampermonkey.net/
// @version      2025-08-05
// @description  use custom dark style for SYZOJ
// @author       XuYueming
// @match        http://?/*
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    function copyText(text) {
        return new Promise((resolve, reject) => {
            if (!text) {
                reject('nothing copyied!');
            }
            const $textarea = $('<textarea>');
            $('body').append($textarea);
            $textarea.val(text).select();
            try {
                const successful = document.execCommand('copy');
                $textarea.remove();
                if (successful) {
                    resolve();
                } else {
                    reject();
                }
            } catch (err) {
                $textarea.remove();
                reject(err);
            }
        });
    }

    const createEditorIframe = function (code, lang, $parent, config) {
        const $iframe = $('<iframe>')
            .attr('class', 'custom-editor')
            .appendTo($parent);
        const iframe = $iframe[0];
        const win = iframe.contentWindow;
        const doc = iframe.contentDocument || iframe.contentWindow.document;

        doc.open();
        doc.write(`
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <style>
                html, body, #container {
                    margin: 0;
                    height: 100%;
                    width: 100%;
                    overflow: hidden;
                }
            </style>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
        </head>
        <body>
            <div id="container"></div>
        </body>
        </html>
        `);
        doc.close();

        return new Promise((resolve) => {
            const tryInit = () => {
                if (typeof win.require === 'undefined') {
                    setTimeout(tryInit, 50); // retry
                    return;
                }

                win.require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.44.0/min/vs' } });
                win.require(['vs/editor/editor.main'], function () {
                    const editor = win.monaco.editor.create(doc.getElementById('container'), {
                        value: code,
                        language: lang,
                        theme: 'vs-dark',
                        contextmenu: false,
                        renderFinalNewline: true,
                        scrollbar: {
                            alwaysConsumeMouseWheel: false,
                        },
                        ...config
                    });

                    resolve({
                        $iframe: $iframe,
                        editor: editor,
                        winMonaco: win.monaco
                    });
                });
            };

            tryInit();
        });
    };

    const problemEditor = async function () {
        const code = unsafeWindow.editor.getValue();
        const lang = unsafeWindow.editor.getModel().getLanguageIdentifier().language;

        const { $iframe, editor, winMonaco } = await createEditorIframe(code, lang, $('#editor'));

        unsafeWindow.editor.dispose();
        unsafeWindow.editor = editor;

        $('#languages-menu .item').click(function () {
            winMonaco.editor.setModelLanguage(editor.getModel(), $(this).data('mode'));
        });
    };

    const submissionEditor = async function () {
        const script = document.createElement('script');
        script.textContent = `
            // 尝试暴露页面中的变量
            (function() {
                function decodeHtmlEntities(str) {
                    const doc = new DOMParser().parseFromString(str, 'text/html');
                    return doc.documentElement.textContent;
                }
                window._exposed = {
                    formattedCode: formattedCode && decodeHtmlEntities(formattedCode),
                    unformattedCode: unformattedCode && decodeHtmlEntities(unformattedCode),
                    language: vueApp.roughData.info['language']
                };
            })();
        `;
        document.documentElement.appendChild(script);
        script.remove();

        await new Promise(resolve => setTimeout(resolve, 50));
        const { formattedCode, unformattedCode, language } = unsafeWindow._exposed;

        let lang = undefined;

        if (language.includes('C++')) {
            lang = 'cpp';
        } else if (language.includes('Python')) {
            lang = 'python';
        } else {
            console.warn('unknown language', language);
            return;
        }

        const $div = $('<div>');
        $($('#submission_content pre:has(>code)')[0])
            .replaceWith($div);

        const { $iframe, editor } = await createEditorIframe(formattedCode || unformattedCode, lang, $div,
            {
                minimap: { enabled: false },
                scrollBeyondLastLine: false,
                readOnly: true,
                automaticLayout: false,
                scrollbar: {
                    vertical: 'hidden',
                    alwaysConsumeMouseWheel: false,
                },
            }
        );

        const updateHeight = () => {
            const contentHeight = editor.getContentHeight() + 5;
            $div.css({ height: `${contentHeight}px` });
            editor.layout();
        };

        updateHeight();

        if (formattedCode) {
            const $btn = $div.parent().find('>a');
            let formatted = true;
            $btn.on('click', () => {
                formatted = !formatted;
                editor.setValue(formatted ? formattedCode : unformattedCode);
                updateHeight();
            });
        }

        editor.focus();
    };

    const copyMarkdown = async function () {
        function getHeaderLevel($x) {
            const m = $x.prop("tagName").match(/^H([1-6])$/);
            return m ? +m[1] : null;
        }
        function tableToMarkdownWithSpan($table) {
            const grid = [];
            let maxCols = 0;

            $table.find('tr').each(function (rowIndex) {
                grid[rowIndex] = grid[rowIndex] || [];
                let colIndex = 0;

                $(this).children('th, td').each(function () {
                    // 跳过已占用的位置
                    while (grid[rowIndex][colIndex]) colIndex++;

                    const cell = $(this);
                    const text = getText(cell).trim().replace(/\n/g, '<br>');
                    const rowspan = parseInt(cell.attr('rowspan') || 1);
                    const colspan = parseInt(cell.attr('colspan') || 1);

                    for (let r = 0; r < rowspan; r++) {
                        for (let c = 0; c < colspan; c++) {
                            const targetRow = rowIndex + r;
                            const targetCol = colIndex + c;

                            grid[targetRow] = grid[targetRow] || [];
                            grid[targetRow][targetCol] = text; // 使用相同文本
                        }
                    }

                    colIndex += colspan;
                    maxCols = Math.max(maxCols, colIndex);
                });
            });

            // 构建 Markdown 表格
            let markdown = '';
            for (let i = 0; i < grid.length; i++) {
                const row = grid[i];
                const paddedRow = [];
                for (let j = 0; j < maxCols; j++) {
                    paddedRow.push(row[j] !== undefined ? row[j] : '');
                }
                markdown += '| ' + paddedRow.join(' | ') + ' |\n';

                // 添加分隔线
                if (i === 0) {
                    markdown += '| ' + paddedRow.map(() => '---').join(' | ') + ' |\n';
                }
            }

            return markdown;
        }
        const getText = function ($content) {
            if ($content.is('span.mjpage')) { // math formula
                const math = $content.find('title').text().replace(/^\n+|\n+$/g, ''); // remove '\n' but not ' ', see this $\ $
                return $content.is('.mjpage__block') ? `\n\$\$\n${math}\n\$\$\n` : ` $${math}$ `;
            }
            if ($content.is('code')) { // code
                if ($content.is(':not(pre) > code'))
                    return '`' + $content.text() + '`';
                return '```\n' + $content.text() + '```\n\n';
            }
            if ($content.is('table')) { // table
                return '\n\n' + tableToMarkdownWithSpan($content) + '\n\n';
            }
            if ($content.is('ul') || $content.is('ol')) { // list
                const prefix = $content.is('ul') ? '- ' : '1. ';
                return $content.children().map((_, li) => prefix + getText($(li)).trim().replace(/\n/g, '\n    ')).get().join('\n') + '\n';
            }
            if ($content.is('hr')) { // horizon
                return '------\n';
            }
            if ($content.is('br')) {  // new line
                return '\n';
            }
            let text = "";
            $content.contents().each(function (index, node) {
                if (node.nodeType === 3) {
                    text += node.nodeValue;
                } else if (node.nodeType === 1) {
                    text += getText($(node));
                }
            });
            if ($content.is('strong')) { // strong
                text = `**${text}**`;
            }
            if ($content.is('s')) { // delete
                text = `~~${text}~~`;
            }
            if ($content.is('em')) {  // em
                text = `_${text}_`;
            }
            if (getHeaderLevel($content)) { // header
                text = `${'#'.repeat(getHeaderLevel($content))} ${text}\n\n`;
            }
            if ($content.is('p')) { // paragraph
                text = text + '\n\n';
            }
            if ($content.is('.ui.message')) { // quote
                text = text.split('\n').map(s => s === '' ? '>' : '> ' + s).join('\n') + '\n';
                // must one more \n, because we don't want two quote being merged into one
            }
            if ($content.is('a')) { // link
                return `[${text}](${$content.attr('href')})`;
            }
            if ($content.is('img')) { // image
                return `![](${$content.attr('src')})`;
            }
            return text;
        };
        const createButton = function ($content) {
            return $('<a>')
                .attr('class', 'small ui primary button copyMD')
                .text('copy MD')
                .on('click', function () {
                    if ($(this).data('copy')) {
                        return;
                    }
                    const text = getText($content);
                    console.log(text);
                    // alert(text);
                    $(this).data('copy', true)
                        .text('copying');
                    copyText(text)
                        .then(() => {
                            $(this).text('copyied!');
                        })
                        .catch((err) => {
                            $(this).text(err || 'failed!');
                        })
                        .finally(() => {
                            setTimeout(() => {
                                $(this).data('copy', null)
                                    .text('copy MD');
                            }, 3000);
                        });
                });
        };
        if (window.location.href.includes('problem')) {
            $('.ui.top.attached.header').each(function () {
                $(this).append(createButton(
                    $(this).parent().children().last()
                ));
            });
        }
        if (window.location.href.includes('article')) {
            $('.padding>p:first').append(createButton(
                $('#content')
            ));
            $('.comment').each(function () {
                $(this).find('.metadata').append(createButton(
                    $(this).find('.text')
                ))
            });
        }
        if (window.location.href.includes('user')) {
            const $column = $("div:has(>h4:contains('个性签名'))"); // cannot be absolutely positioned
            $column.find('.header').append(createButton(
                $column.find('.segment')
            ));
        }
    };

    const markdownEditor = async function () {
        $('.markdown-edit').each(function () {
            const $original = $(this);
            const initialVal = $original.val();

            const $container = $('<div>').addClass('markdown-container');
            const $left = $('<div>').addClass('markdown-left');
            const $right = $('<div>').addClass('markdown-right');
            const $textarea = $('<textarea class="markdown-source"></textarea>')
                .val(initialVal).attr('name', $original.attr('name'))
                .attr('id', $original.attr('id'));  // fix article preview error
            const $preview = $('<div>').addClass('markdown-preview');
            const $toggleButton = $('<a>').attr('class', 'small ui primary button toggle-preview').text('切换预览');

            $left.append($toggleButton, $textarea);
            $right.append($preview);
            $container.append($left, $right);
            $original.replaceWith($container);

            function update() {
                $.post('/api/markdown', { s: $textarea.val() }, function (s) {
                    $preview.html(s);
                });
            }

            $textarea.on('input', update);
            update();

            $toggleButton.on('click', function () {
                if ($right.is(':visible')) {
                    $right.hide();
                    $left.addClass('markdown-full');
                } else {
                    $right.show();
                    $left.removeClass('markdown-full');
                }
            });
        });
    };

    const ranklistOverflowFix = async function () {
        const $container = $('.main');
        const $table = $('.table');

        $container.css({
            'padding-left': '10px',
            'overflow-x': 'auto'
        });
        $table.css({
            'transform-origin': 'left top'
        });

        const $button = $('<a>')
            .css('vertical-align', 'bottom')
            .attr('class', 'small ui primary button copyMD')
            .appendTo($container.find('.header'));
        $container.find('.header').prependTo(
            $container.parent()
        );

        const ADJUST = 30;

        const _wrapperWidth = $container.width();
        const _tableWidth = $table.outerWidth() + ADJUST;
        const _scale = _wrapperWidth / _tableWidth;
        let flag = _scale < .9;

        const layout = function () {
            const wrapperWidth = $container.width();
            const tableWidth = $table.outerWidth() + ADJUST;
            const scale = wrapperWidth / tableWidth;
            if (flag) {
                $table.css('transform', 'scale(' + scale + ')');
                $button.text('unscale');
            } else {
                $table.css('transform', '');
                $button.text('scale');
            }
        };

        layout();

        $button.click(() => {
            flag = !flag;
            layout();
        });

        $(window).resize(layout);
    };

    const href = window.location.href;

    if (typeof unsafeWindow.onEditorLoaded === 'function')
        unsafeWindow.onEditorLoaded(problemEditor);
    if (href.includes('submission'))
        $(document).ready(submissionEditor);
    if ($('.markdown-edit').length > 0)
        $(document).ready(markdownEditor);
    if (href.includes('ranklist'))
        $(document).ready(ranklistOverflowFix);
    $(document).ready(copyMarkdown);
})();
posted @ 2025-08-05 22:20  XuYueming  阅读(14)  评论(0)    收藏  举报