// ==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();
});
})();