- // ==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://greatest.deepsurf.us/scripts/422854-bubble-message.js
- // @require https://greatest.deepsurf.us/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();
- });
- })();