Greasy Fork is available in English.

LeetCode Assistant

【使用前先看介绍/有问题可反馈】力扣助手 (LeetCode Assistant):为力扣页面增加辅助功能。

// ==UserScript==
// @name         LeetCode Assistant
// @namespace    http://tampermonkey.net/
// @version      1.0.9
// @description  【使用前先看介绍/有问题可反馈】力扣助手 (LeetCode Assistant):为力扣页面增加辅助功能。
// @author       cc
// @require      https://cdn.bootcss.com/jquery/3.4.1/jquery.js
// @require      https://greasyfork.org/scripts/422854-bubble-message.js
// @require      https://greasyfork.org/scripts/432416-statement-parser.js
// @match        https://leetcode.cn/problems/*
// @match        https://leetcode.cn/problemset/*
// @match        https://leetcode.cn/company/*/*
// @match        https://leetcode.cn/problem-list/*/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==
// noinspection JSUnresolvedFunction

(function() {
    const __VERSION__ = '1.0.8';
    let executing = false;
    const bm = new BubbleMessage();
    bm.config.width = 400;

    const config = {
        recommendVisible: false,
        autoAdjustView: true,
        __hideAnsweredQuestion: false,
        __hideCollectionAnsweredQuestion: false,
        __supportLanguage: ['Java', 'C++', 'Python3', 'JavaScript'],
    };

    const Basic = {
        updateData: function(obj) {
            let data = GM_getValue('data');
            if (!obj) {
                // 初始化调用
                if (!data) {
                    // 未初始化
                    data = {};
                    Object.assign(data, config);
                    GM_setValue('data', data);
                } else {
                    // 已初始化,检查是否存在更新脚本后未添加的值
                    let isModified = false;
                    for (let key in config) {
                        if (data[key] === undefined) {
                            isModified = true;
                            data[key] = config[key];
                        }
                    }
                    // 双下划綫开头的属性删除掉,因为不需要保存
                    for (let key in data) {
                        if (key.startsWith('__')) {
                            isModified = true;
                            delete data[key];
                        }
                    }
                    if (isModified)
                        GM_setValue('data', data);
                    Object.assign(config, data);
                }
            } else {
                // 更新调用
                Object.assign(config, obj);
                Object.assign(data, config);
                GM_setValue('data', data);
            }
        },
        listenHistoryState: function() {
            const _historyWrap = function(type) {
                const orig = history[type];
                const e = new Event(type);
                return function() {
                    const rv = orig.apply(this, arguments);
                    e.arguments = arguments;
                    window.dispatchEvent(e);
                    return rv;
                };
            };
            history.pushState = _historyWrap('pushState');
            window.addEventListener('pushState', () => {
                if (!executing) {
                    executing = true;
                    main();
                }
            });
        },
        observeChildList: function(node, callback) {
            let observer = new MutationObserver(function(mutations) {
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        callback([...mutation.addedNodes]);
                    }
                });
            });
            observer.observe(node, { childList: true });
        },
        executeUtil: function(task, cond, args, thisArg, timeout) {
            args = args || [];
            timeout = timeout || 250;
            if (cond()) {
                task.apply(thisArg, args);
            } else {
                setTimeout(() => {
                    Basic.executeUtil(task, cond, args, thisArg, timeout);
                }, timeout);
            }
        }
    };

    const Switch = {
        setSwitch: function(container, id_, onchange, text, defaultChecked) {
            if (defaultChecked === undefined)
                defaultChecked = true;
            container.style = 'display: inline-flex; align-items: center; margin-left: 10px;';
            let switchCheckbox = document.createElement('input');
            switchCheckbox.type = 'checkbox';
            switchCheckbox.checked = defaultChecked;
            switchCheckbox.setAttribute('id', id_);
            switchCheckbox.addEventListener('change', onchange);
            let switchLabel = document.createElement('label');
            switchLabel.setAttribute('for', id_);
            switchLabel.innerText = text;
            switchLabel.style.marginLeft = '5px';
            switchLabel.setAttribute('style', 'margin-left: 5px; cursor: default;')
            container.appendChild(switchCheckbox);
            container.appendChild(switchLabel);
        },
        switchVisible: function(nodes, visible, defaultDisplay) {
            defaultDisplay = defaultDisplay || '';
            if (visible) {
                nodes.forEach(node => node.style.display = defaultDisplay);
            } else {
                nodes.forEach(node => node.style.display = 'none');
            }
        },
        switchRecommendVisible: function() {
            let nodes = [];
            let target = document.querySelector('.border-divider-border-2');
            while (target) {
                nodes.push(target);
                target = target.previousElementSibling;
            }
            let sidebar = document.querySelector('.col-span-4:nth-child(2)');
            target = sidebar.querySelector('.space-y-4:nth-child(2)');
            while (target) {
                nodes.push(target);
                target = target.nextElementSibling;
            }
            Switch.switchVisible(nodes, config.recommendVisible);
            Basic.observeChildList(sidebar, (nodes) => {
                Switch.switchVisible(nodes, config.recommendVisible);
            });
        },
        switchAnsweredQuestionVisible: function() {
            let rowGroup = document.querySelector('[role=rowgroup]');
            let nodes = [...rowGroup.querySelectorAll('[role=row]')];
            let matchPage = location.href.match(/\?page=(\d+)/);
            if (!matchPage || parseInt(matchPage[1]) === 1)
                nodes = nodes.slice(1);
            nodes = nodes.filter(node => node.querySelector('svg.text-green-s'));
            Switch.switchVisible(nodes, !config.__hideAnsweredQuestion, 'flex');
        },
        switchCollectionAnsweredQuestionVisible: function() {
            let nodes = [...document.querySelectorAll('.ant-table-tbody>tr')];
            nodes = nodes.filter(node => {
                let svg = node.querySelector('svg');
                return svg.getAttribute('color').includes('success');
            });
            Switch.switchVisible(nodes, !config.__hideCollectionAnsweredQuestion);
        }
    };

    const Insert = {
        base: {
            insertStyle: function() {
                if (document.getElementById('leetcode-assistant-style'))
                    return;
                let style = document.createElement('style');
                style.setAttribute('id', 'leetcode-assistant-style');
                style.innerText = `
                    .leetcode-assistant-copy-example-button {
                        border: 1px solid;
                        border-radius: 2px;
                        cursor: pointer;
                        padding: 1px 4px;
                        font-size: 0.8em;
                        margin-top: 5px;
                        width: fit-content;
                    }
                    .leetcode-assistant-highlight-accept-submission {
                        font-weight: bold;
                    }`;
                document.body.appendChild(style);
            },
            insertTextarea: function() {
                let textarea = document.createElement('textarea');
                textarea.setAttribute('id', 'leetcode-assistant-textarea');
                textarea.setAttribute('style', 'width: 0; height: 0;')
                document.body.appendChild(textarea);
            }
        },
        copy: {
            insertCopyStructCode: function() {
                const id_ = 'leetcode-assistant-copy-struct-button';
                if (document.getElementById(id_)) {
                    executing = false;
                    return;
                }
                let buttonContainer = document.querySelector('[class^=first-section-container]');
                let ref = buttonContainer.querySelector('button:nth-child(2)');
                let button = document.createElement('button');
                button.setAttribute('id', id_);
                button.className = ref.className;
                let span = document.createElement('span');
                span.className = ref.lastElementChild.className;
                span.innerText = '复制结构';
                button.appendChild(span);
                button.addEventListener('click', Copy.copyClassStruct);
                buttonContainer.appendChild(button);
                executing = false;
            },
            insertCopySubmissionCode: function() {
                let tbody = document.querySelector('.ant-table-tbody');
                let trs = [...tbody.querySelectorAll('tr')];
                let processTr = (tr) => {
                    let qid = tr.dataset.rowKey;
                    Basic.executeUtil((tr) => {
                        let cell = tr.querySelector(':nth-child(4)');
                        cell.title = '点击复制代码';
                        cell.style = 'cursor: pointer; color: #007aff';
                        cell.addEventListener('click', function() {
                            XHR.requestCode(qid);
                        });
                        cell.setAttribute('data-set-copy', 'true');
                    }, () => {
                        let cell = tr.querySelector(':nth-child(4)');
                        return cell && cell.dataset.setCopy !== 'true';
                    }, [tr]);
                }
                trs.forEach(processTr);
                Fun.highlightBestAcceptSubmission();
                Basic.observeChildList(tbody, (nodes) => {
                    let node = nodes[0];
                    if (node.tagName === 'TR') {
                        processTr(node);
                        Fun.highlightBestAcceptSubmission();
                    }
                });
                executing = false;
            },
            insertCopyExampleInput: function() {
                // 检查是否添加 "复制示例代码" 按钮
                let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
                if (content.dataset.addedCopyExampleInputButton === 'true')
                    return;
                // 对每个 example 添加复制按钮
                let examples = [...content.querySelectorAll('pre')];
                for (let example of examples) {
                    let btn = document.createElement('div');
                    btn.innerText = '复制示例输入';
                    btn.className = 'leetcode-assistant-copy-example-button';
                    btn.addEventListener('click', () => {
                        Copy.copyExampleInput(example);
                    });
                    example.appendChild(btn);
                }
                content.setAttribute('data-added-copy-example-input-button', 'true');
                executing = false;
            },
            insertCopyTestInput: function() {
                function addCopyTestInputForInputInfo(inputInfo) {
                    inputInfo = inputInfo || document.querySelector('[class^=result-container] [class*=ValueContainer]');
                    if (inputInfo && inputInfo.dataset.setCopy !== 'true') {
                        inputInfo.addEventListener('click', function() {
                            // 检查是否支持语言
                            let lang = Get.getLanguage();
                            if (!config.__supportLanguage.includes(lang)) {
                                bm.message({
                                    type: 'warning',
                                    message: '目前不支持该语言的测试输入代码复制',
                                    duration: 1500,
                                });
                                executing = false;
                                return;
                            }
                            // 主要代码
                            let sp = new StatementParser(lang);
                            let expressions = this.innerText.trim().split('\n');
                            let declares = sp.getDeclaresFromCode(Get.getCode());
                            let statements = sp.getStatementsFromDeclaresAndExpressions(declares, expressions);
                            Copy.copy(statements);
                        });
                        inputInfo.setAttribute('data-set-copy', 'true');
                    }
                }
                let submissions = document.querySelector('[class^=submissions]');
                submissions.addEventListener('DOMNodeInserted', function(event) {
                    if (event.target.className.startsWith('container') || event.target.className.includes('Container')) {
                        Basic.executeUtil((container) => {
                            let inputInfo = container.querySelector('[class*=ValueContainer]');
                            addCopyTestInputForInputInfo(inputInfo);
                        }, () => {
                            return event.target.querySelector('[class*=ValueContainer]');
                        }, [event.target]);
                    }
                });
                addCopyTestInputForInputInfo();
                executing = false;
            },
        },
        switch: {
            insertRecommendVisibleSwitch: function() {
                const id_ = 'leetcode-assistant-recommend-visible-switch';
                if (document.getElementById(id_)) {
                    executing = false;
                    return;
                }
                let container = document.querySelector('.relative.space-x-5').nextElementSibling;
                let onchange = function() {
                    Basic.updateData({ recommendVisible: !this.checked });
                    Switch.switchRecommendVisible();
                };
                let text = '简洁模式';
                Switch.setSwitch(container, id_, onchange, text);
                executing = false;
            },
            insertHideAnsweredQuestionSwitch: function() {
                const id_ = 'leetcode-assistant-hide-answered-question-switch';
                if (document.getElementById(id_)) {
                    executing = false;
                    return;
                }
                let container = document.createElement('div');
                document.querySelector('.relative.space-x-5').parentElement.appendChild(container);
                let onchange = function() {
                    config.__hideAnsweredQuestion = !config.__hideAnsweredQuestion;
                    Switch.switchAnsweredQuestionVisible();
                };
                let text = '隐藏已解决';
                Switch.setSwitch(container, id_, onchange, text, false);
                Basic.executeUtil(() => {
                    let btns = [...document.querySelectorAll('[role=navigation] button')];
                    btns.forEach(btn => {
                        btn.addEventListener("click", function() {
                            document.getElementById(id_).checked = false;
                            config.__hideAnsweredQuestion = false;
                            Switch.switchAnsweredQuestionVisible();
                            return true;
                        });
                    });
                }, () => {
                    let btns = [...document.querySelectorAll('[role=navigation] button')];
                    return btns.length > 0;
                });
                executing = false;
            },
            insertHideCollectionAnsweredQuestionSwitch: function() {
                const id_ = 'leetcode-assistant-hide-collection-answered-question-switch';
                if (document.getElementById(id_)) {
                    executing = false;
                    return;
                }
                let container = document.createElement('div');
                document.querySelector('#lc-header>nav>ul:first-child').appendChild(container);
                let onchange = function() {
                    config.__hideCollectionAnsweredQuestion = !config.__hideCollectionAnsweredQuestion;
                    Switch.switchCollectionAnsweredQuestionVisible();
                };
                let text = '隐藏已解决';
                Switch.setSwitch(container, id_, onchange, text, false);
                Basic.executeUtil(() => {
                    let btns = [...document.querySelectorAll('.ant-table-pagination li>*')];
                    btns = btns.filter(btn => btn.tagName === 'BUTTON' || btn.tagName === 'A');
                    btns.forEach(btn => {
                        btn.addEventListener('click', function() {
                            document.getElementById(id_).checked = false;
                            config.__hideCollectionAnsweredQuestion = false;
                            Switch.switchCollectionAnsweredQuestionVisible();
                            return true;
                        });
                    });
                }, () => {
                    let btns = [...document.querySelectorAll('.ant-pagination-item')];
                    return btns.length > 0;
                });
                executing = false;
            },
            insertAutoAdjustViewSwitch: function() {
                const id_ = 'leetcode-assistant-auto-adjust-view-switch';
                if (document.getElementById(id_)) {
                    executing = false;
                    return;
                }
                let container = document.querySelector('[data-status] nav > ul');
                let onchange = function() {
                    Basic.updateData({ autoAdjustView: this.checked });
                };
                let text = '自动调节视图';
                Switch.setSwitch(container, id_, onchange, text);
                executing = false;
            }
        }
    };

    const Copy = {
        copy: function(value) {
            let textarea = document.getElementById('leetcode-assistant-textarea');
            textarea.value = value;
            textarea.setAttribute('value', value);
            textarea.select();
            document.execCommand('copy');
            bm.message({
                type: 'success',
                message: '复制成功',
                duration: 1500,
            });
        },
        copyClassStruct: function() {
            // 检查语言是否支持
            let lang = Get.getLanguage();
            if (!config.__supportLanguage.includes(lang)) {
                bm.message({
                    type: 'warning',
                    message: '目前不支持该语言的结构类代码复制',
                    duration: 1500,
                });
                executing = false;
                return;
            }
            // 主要代码
            let sp = new StatementParser(lang);
            let classStructCode = sp.getClassStructFromCode(Get.getCode());
            if (!classStructCode) {
                bm.message({
                    type: 'warning',
                    message: '结构类代码不存在',
                    duration: 1500,
                });
                return;
            }
            Copy.copy(classStructCode);
        },
        copyExampleInput: function(example) {
            // 检查语言是否支持
            let lang = Get.getLanguage();
            if (!config.__supportLanguage.includes(lang)) {
                bm.message({
                    type: 'warning',
                    message: '目前不支持该语言的示例输入代码复制',
                    duration: 1500,
                });
                executing = false;
                return;
            }
            let sp = new StatementParser(lang);
            // 获取 declares
            let declares = sp.getDeclaresFromCode(Get.getCode());
            // 获取 expressions
            let strong = example.querySelector('strong');
            let inputText = "";
            if (strong && strong.nextSibling) {
                let inputTextElement = strong.nextSibling;
                while ((inputTextElement instanceof Text) || !['STRONG', 'B'].includes(inputTextElement.tagName)) {
                    if (inputTextElement instanceof Text) {
                        inputText += inputTextElement.wholeText;
                    } else {
                        inputText += inputTextElement.innerText;
                    }
                    inputTextElement = inputTextElement.nextSibling;
                }
            } else {
                inputText = example.innerText.replace(/\n/g, '').match(/输入:(.+)输出:/)[1];
            }
            let expressions = inputText.trim().replace(/,$/, '');
            if (inputText.replace(/".+?"/g, '').includes(',')) {
                // 无视字符串后存在逗号分隔符,说明有多个输入
                expressions = expressions.split(/,\s+/);
            } else {
                // 单个输入
                expressions = [expressions];
            }
            // 生成语句并复制
            Copy.copy(sp.getStatementsFromDeclaresAndExpressions(declares, expressions));
        },
    };

    const XHR = {
        requestCode: function(qid) {
            let query = `
                query mySubmissionDetail($id: ID!) {
                  submissionDetail(submissionId: $id) {
                    id
                    code
                    runtime
                    memory
                    rawMemory
                    statusDisplay
                    timestamp
                    lang
                    passedTestCaseCnt
                    totalTestCaseCnt
                    sourceUrl
                    question {
                      titleSlug
                      title
                      translatedTitle
                      questionId
                      __typename
                    }
                    ... on GeneralSubmissionNode {
                      outputDetail {
                        codeOutput
                        expectedOutput
                        input
                        compileError
                        runtimeError
                        lastTestcase
                        __typename
                      }
                      __typename
                    }
                    submissionComment {
                      comment
                      flagType
                      __typename
                    }
                    __typename
                  }
                }`;
            $.ajax({
                url: 'https://leetcode.cn/graphql/',
                method: 'POST',
                contentType: 'application/json',
                data: JSON.stringify({
                    operationName: 'mySubmissionDetail',
                    query: query,
                    variables: {
                        id: qid
                    },
                }),
            }).then(res => {
                Copy.copy(res.data.submissionDetail.code);
            });
        }
    };

    const Get = {
        getLanguage: function() {
            return document.getElementById('lang-select').innerText;
        },
        getCode: function() {
            return document.querySelector('[name=code]').value;
        }
    };

    const Fun = {
        adjustViewScale: function(left, right) {
            if (!config.autoAdjustView) {
                executing = false;
                return;
            }
            let splitLine = document.querySelector('[data-is-collapsed]');
            let leftPart = splitLine.previousElementSibling;
            let rightPart = splitLine.nextElementSibling;
            let leftPartFlex = leftPart.style.flex.match(/\d+\.\d+/)[0];
            let rightPartFlex = rightPart.style.flex.match(/\d+\.\d+/)[0];
            leftPart.style.flex = leftPart.style.flex.replace(leftPartFlex, `${left}`);
            rightPart.style.flex = rightPart.style.flex.replace(rightPartFlex, `${right}`);
            executing = false;
        },
        highlightBestAcceptSubmission: function() {
            let highlightClassName = 'leetcode-assistant-highlight-accept-submission';
            let items = [...document.querySelectorAll('tr[data-row-key]')];
            let acItems = items.filter(item => item.querySelector('a[class^=ac]'));
            if (acItems.length === 0)
                return;
            let matchTimeMem = acItems.map(item => item.innerText.match(/(\d+)\sms.+?(\d+\.?\d)\sMB/).slice(1, 3));
            let timeList = matchTimeMem.map(res => parseInt(res[0]));
            let memList = matchTimeMem.map(res => parseFloat(res[1]));
            let targetIndex = 0;
            for (let i = 0; i < items.length; i++) {
                if (timeList[i] < timeList[targetIndex] || (timeList[i] === timeList[targetIndex] && memList[i] < memList[targetIndex])) {
                    targetIndex = i;
                }
            }
            let lastTarget = document.querySelector(`.${highlightClassName}`);
            if (lastTarget)
                lastTarget.classList.remove(highlightClassName);
            acItems[targetIndex].classList.add(highlightClassName);
        }
    };

    function main() {
        console.log(`LeetCode Assistant version ${__VERSION__}`);
        if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\//)) { // /problems/*
            Basic.executeUtil(() => {
                Insert.copy.insertCopyStructCode();
                Insert.switch.insertAutoAdjustViewSwitch();
            }, () => {
                return document.querySelector('[class^=first-section-container]');
            });
            if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\/$/)) { // 题目描述
                Fun.adjustViewScale(0.618, 0.382);
                Basic.executeUtil(Insert.copy.insertCopyExampleInput, () => {
                    let codeDOM = document.querySelector('.editor-scrollable');
                    let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
                    return codeDOM && content && content.querySelector('pre');
                });
            } else if (location.href.includes('/solution/')) { // 题解
                Fun.adjustViewScale(0.382, 0.618);
            } else if (location.href.includes('/submissions/')) { // 提交记录
                Basic.executeUtil(() => {
                    Insert.copy.insertCopySubmissionCode();
                    Insert.copy.insertCopyTestInput();
                }, () => {
                    return document.querySelector('.ant-table-thead');
                });
            }
        } else if (location.href.startsWith('https://leetcode.cn/problemset/')) { // 首页
            Insert.switch.insertRecommendVisibleSwitch();
            Switch.switchRecommendVisible();
            Basic.executeUtil(() => {
                Insert.switch.insertHideAnsweredQuestionSwitch();
                Switch.switchAnsweredQuestionVisible();
            }, () => {
                let navigation = document.querySelector('[role=navigation]');
                return navigation && navigation.innerText.length > 0;
            });
        } else if (location.href.startsWith('https://leetcode.cn/problem-list/') || location.href.startsWith('https://leetcode.cn/company/')) { // 集合类型问题列表页
            Insert.switch.insertHideCollectionAnsweredQuestionSwitch();
            Basic.executeUtil(() => {
                Insert.switch.insertHideCollectionAnsweredQuestionSwitch();
                Switch.switchCollectionAnsweredQuestionVisible();
            }, () => {
                let navigation = document.querySelector('#lc-header>nav>ul');
                return navigation && navigation.childElementCount > 0;
            });
        } else {
            executing = false;
        }
    }

    window.addEventListener('load', () => {
        Basic.updateData();
        Insert.base.insertStyle();
        Insert.base.insertTextarea();
        Basic.listenHistoryState();
        main();
    });
})();