Greasy Fork is available in English.

LeetCode Assistant

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

Fra 14.09.2021. Se den seneste versjonen.

  1. // ==UserScript==
  2. // @name LeetCode Assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.2
  5. // @description 【使用前先看介绍/有问题可反馈】力扣助手 (LeetCode Assistant):为力扣页面增加辅助功能。
  6. // @author cc
  7. // @require https://cdn.bootcss.com/jquery/3.4.1/jquery.js
  8. // @require https://greatest.deepsurf.us/scripts/422854-bubble-message.js
  9. // @require https://greatest.deepsurf.us/scripts/432416-statement-parser.js
  10. // @match https://leetcode-cn.com/problems/*
  11. // @match https://leetcode-cn.com/problemset/*
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. let executing = false;
  18. const bm = new BubbleMessage();
  19. bm.config.width = 400;
  20.  
  21. const config = {
  22. recommendVisible: false,
  23. autoAdjustView: true,
  24. __hideAnsweredQuestion: false,
  25. __supportLanguage: ['Java', 'C++', 'Python3'],
  26. };
  27.  
  28. const Basic = {
  29. updateData: function(obj) {
  30. let data = GM_getValue('data');
  31. if (!obj) {
  32. // 初始化调用
  33. if (!data) {
  34. // 未初始化
  35. data = {};
  36. Object.assign(data, config);
  37. GM_setValue('data', data);
  38. } else {
  39. // 已初始化,检查是否存在更新脚本后未添加的值
  40. let isModified = false;
  41. for (let key in config) {
  42. if (data[key] === undefined) {
  43. isModified = true;
  44. data[key] = config[key];
  45. }
  46. }
  47. // 双下划綫开头的属性删除掉,因为不需要保存
  48. for (let key in data) {
  49. if (key.startsWith('__')) {
  50. isModified = true;
  51. delete data[key];
  52. }
  53. }
  54. if (isModified)
  55. GM_setValue('data', data);
  56. Object.assign(config, data);
  57. }
  58. } else {
  59. // 更新调用
  60. Object.assign(config, obj);
  61. Object.assign(data, config);
  62. GM_setValue('data', data);
  63. }
  64. },
  65. listenHistoryState: function() {
  66. const _historyWrap = function(type) {
  67. const orig = history[type];
  68. const e = new Event(type);
  69. return function() {
  70. const rv = orig.apply(this, arguments);
  71. e.arguments = arguments;
  72. window.dispatchEvent(e);
  73. return rv;
  74. };
  75. };
  76. history.pushState = _historyWrap('pushState');
  77. window.addEventListener('pushState', () => {
  78. console.log('history is', history);
  79. console.log('executing is', executing);
  80. if (!executing) {
  81. executing = true;
  82. main();
  83. }
  84. });
  85. },
  86. observeChildList: function(node, callback) {
  87. let observer = new MutationObserver(function(mutations) {
  88. mutations.forEach((mutation) => {
  89. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  90. callback([...mutation.addedNodes]);
  91. }
  92. });
  93. });
  94. observer.observe(node, { childList: true });
  95. },
  96. executeUtil: function(task, cond, args, thisArg, timeout) {
  97. args = args || [];
  98. timeout = timeout || 250;
  99. if (cond()) {
  100. task.apply(thisArg, args);
  101. } else {
  102. setTimeout(() => {
  103. Basic.executeUtil(task, cond, args, thisArg, timeout);
  104. }, timeout);
  105. }
  106. }
  107. };
  108.  
  109. const Switch = {
  110. setSwitch: function(container, id_, onchange, text, defaultChecked) {
  111. if (defaultChecked === undefined)
  112. defaultChecked = true;
  113. container.style = 'display: inline-flex; align-items: center; margin-left: 10px;';
  114. let switchCheckbox = document.createElement('input');
  115. switchCheckbox.type = 'checkbox';
  116. switchCheckbox.checked = defaultChecked;
  117. switchCheckbox.setAttribute('id', id_);
  118. switchCheckbox.addEventListener('change', onchange);
  119. let switchLabel = document.createElement('label');
  120. switchLabel.setAttribute('for', id_);
  121. switchLabel.innerText = text;
  122. switchLabel.style.marginLeft = '5px';
  123. switchLabel.setAttribute('style', 'margin-left: 5px; cursor: default;')
  124. container.appendChild(switchCheckbox);
  125. container.appendChild(switchLabel);
  126. },
  127. switchVisible: function switchVisible(nodes, visible, defaultDisplay) {
  128. defaultDisplay = defaultDisplay || '';
  129. if (visible) {
  130. nodes.forEach(node => node.style.display = defaultDisplay);
  131. } else {
  132. nodes.forEach(node => node.style.display = 'none');
  133. }
  134. },
  135. switchRecommendVisible: function() {
  136. let nodes = [];
  137. let target = document.querySelector('.border-divider-border-2');
  138. while (target) {
  139. nodes.push(target);
  140. target = target.previousElementSibling;
  141. }
  142. let sidebar = document.querySelector('.col-span-4:nth-child(2)');
  143. target = sidebar.querySelector('.space-y-4:nth-child(2)');
  144. while (target) {
  145. nodes.push(target);
  146. target = target.nextElementSibling;
  147. }
  148. Switch.switchVisible(nodes, config.recommendVisible);
  149. Basic.observeChildList(sidebar, (nodes) => {
  150. Switch.switchVisible(nodes, config.recommendVisible);
  151. });
  152. },
  153. switchAnsweredQuestionVisible: function() {
  154. let rowGroup = document.querySelector('[role=rowgroup]');
  155. let nodes = [...rowGroup.querySelectorAll('[role=row]')].slice(1);
  156. nodes = nodes.filter(node => node.querySelector('svg.text-green-s'));
  157. Switch.switchVisible(nodes, !config.__hideAnsweredQuestion, 'flex');
  158. }
  159. };
  160.  
  161. const Insert = {
  162. base: {
  163. insertStyle: function() {
  164. if (document.getElementById('leetcode-assistant-style'))
  165. return;
  166. let style = document.createElement('style');
  167. style.setAttribute('id', 'leetcode-assistant-style');
  168. style.innerText = `
  169. .leetcode-assistant-copy-example-button {
  170. border: 1px solid;
  171. border-radius: 2px;
  172. cursor: pointer;
  173. padding: 1px 4px;
  174. font-size: 0.8em;
  175. margin-top: 5px;
  176. width: fit-content;
  177. }
  178. .leetcode-assistant-highlight-accept-submission {
  179. font-weight: bold;
  180. }`;
  181. document.body.appendChild(style);
  182. },
  183. insertTextarea: function() {
  184. let textarea = document.createElement('textarea');
  185. textarea.setAttribute('id', 'leetcode-assistant-textarea');
  186. textarea.setAttribute('style', 'width: 0; height: 0;')
  187. document.body.appendChild(textarea);
  188. }
  189. },
  190. copy: {
  191. insertCopyStructCode: function() {
  192. const id_ = 'leetcode-assistant-copy-struct-button';
  193. if (document.getElementById(id_)) {
  194. executing = false;
  195. return;
  196. }
  197. let buttonContainer = document.querySelector('[class^=first-section-container]');
  198. let ref = buttonContainer.querySelector('button:nth-child(2)');
  199. let button = document.createElement('button');
  200. button.setAttribute('id', id_);
  201. button.className = ref.className;
  202. let span = document.createElement('span');
  203. span.className = ref.lastElementChild.className;
  204. span.innerText = '复制结构';
  205. button.appendChild(span);
  206. button.addEventListener('click', Copy.copyClassStruct);
  207. buttonContainer.appendChild(button);
  208. executing = false;
  209. },
  210. insertCopySubmissionCode: function() {
  211. let tbody = document.querySelector('.ant-table-tbody');
  212. let trs = [...tbody.querySelectorAll('tr')];
  213. let processTr = (tr) => {
  214. let qid = tr.dataset.rowKey;
  215. Basic.executeUtil((tr) => {
  216. let cell = tr.querySelector(':nth-child(4)');
  217. cell.title = '点击复制代码';
  218. cell.style = 'cursor: pointer; color: #007aff';
  219. cell.addEventListener('click', function() {
  220. XHR.requestCode(qid);
  221. });
  222. cell.setAttribute('data-set-copy', 'true');
  223. }, () => {
  224. let cell = tr.querySelector(':nth-child(4)');
  225. return cell && cell.dataset.setCopy !== 'true';
  226. }, [tr]);
  227. }
  228. trs.forEach(processTr);
  229. Fun.highlightBestAcceptSubmission();
  230. Basic.observeChildList(tbody, (nodes) => {
  231. let node = nodes[0];
  232. if (node.tagName === 'TR') {
  233. processTr(node);
  234. Fun.highlightBestAcceptSubmission();
  235. }
  236. });
  237. executing = false;
  238. },
  239. insertCopyExampleInput: function() {
  240. // 检查是否添加 "复制示例代码" 按钮
  241. let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
  242. if (content.dataset.addedCopyExampleInputButton === 'true')
  243. return;
  244. // 对每个 example 添加复制按钮
  245. let examples = [...content.querySelectorAll('pre')];
  246. for (let example of examples) {
  247. let btn = document.createElement('div');
  248. btn.innerText = '复制示例输入';
  249. btn.className = 'leetcode-assistant-copy-example-button';
  250. btn.addEventListener('click', () => {
  251. Copy.copyExampleInput(example);
  252. });
  253. example.appendChild(btn);
  254. }
  255. content.setAttribute('data-added-copy-example-input-button', 'true');
  256. executing = false;
  257. },
  258. insertCopyTestInput: function() {
  259. function addCopyTestInputForInputInfo(inputInfo) {
  260. inputInfo = inputInfo || document.querySelector('[class^=result-container] [class*=ValueContainer]');
  261. if (inputInfo && inputInfo.dataset.setCopy !== 'true') {
  262. inputInfo.addEventListener('click', function() {
  263. // 检查是否支持语言
  264. let lang = Get.getLanguage();
  265. if (!config.__supportLanguage.includes(lang)) {
  266. bm.message({
  267. type: 'warning',
  268. message: '目前不支持该语言的测试输入代码复制',
  269. duration: 1500,
  270. });
  271. executing = false;
  272. return;
  273. }
  274. // 主要代码
  275. let sp = new StatementParser(lang);
  276. let expressions = this.innerText.trim().split('\n');
  277. let declares = sp.getDeclaresFromCode(Get.getCode());
  278. let statements = sp.getStatementsFromDeclaresAndExpressions(declares, expressions);
  279. Copy.copy(statements);
  280. });
  281. inputInfo.setAttribute('data-set-copy', 'true');
  282. }
  283. }
  284. let submissions = document.querySelector('[class^=submissions]');
  285. submissions.addEventListener('DOMNodeInserted', function(event) {
  286. if (event.target.className.startsWith('container') || event.target.className.includes('Container')) {
  287. Basic.executeUtil((container) => {
  288. let inputInfo = container.querySelector('[class*=ValueContainer]');
  289. addCopyTestInputForInputInfo(inputInfo);
  290. }, () => {
  291. return event.target.querySelector('[class*=ValueContainer]');
  292. }, [event.target]);
  293. }
  294. });
  295. addCopyTestInputForInputInfo();
  296. executing = false;
  297. },
  298. },
  299. switch: {
  300. insertRecommendVisibleSwitch: function() {
  301. const id_ = 'leetcode-assistant-recommend-visible-switch';
  302. if (document.getElementById(id_)) {
  303. executing = false;
  304. return;
  305. }
  306. let container = document.querySelector('.relative.space-x-5').nextElementSibling;
  307. let onchange = function() {
  308. Basic.updateData({ recommendVisible: !this.checked });
  309. Switch.switchRecommendVisible();
  310. };
  311. let text = '简洁模式';
  312. Switch.setSwitch(container, id_, onchange, text);
  313. executing = false;
  314. },
  315. insertHideAnsweredQuestionSwitch: function() {
  316. const id_ = 'leetcode-assistant-hide-answered-question-switch';
  317. if (document.getElementById(id_)) {
  318. executing = false;
  319. return;
  320. }
  321. let container = document.createElement('div');
  322. document.querySelector('.relative.space-x-5').parentElement.appendChild(container);
  323. let onchange = function() {
  324. config.__hideAnsweredQuestion = !config.__hideAnsweredQuestion;
  325. Switch.switchAnsweredQuestionVisible();
  326. };
  327. let text = '隐藏已解决';
  328. Switch.setSwitch(container, id_, onchange, text, false);
  329. let navigation = document.querySelector('[role=navigation]');
  330. let btns = [...navigation.querySelectorAll('button')];
  331. btns.forEach(btn => {
  332. btn.addEventListener("click", function() {
  333. document.getElementById(id_).checked = false;
  334. config.__hideAnsweredQuestion = false;
  335. Switch.switchAnsweredQuestionVisible();
  336. return true;
  337. });
  338. });
  339. executing = false;
  340. },
  341. insertAutoAdjustViewSwitch: function() {
  342. const id_ = 'leetcode-assistant-auto-adjust-view-switch';
  343. if (document.getElementById(id_)) {
  344. executing = false;
  345. return;
  346. }
  347. let container = document.querySelector('[data-status] nav > ul');
  348. let onchange = function() {
  349. Basic.updateData({ autoAdjustView: this.checked });
  350. };
  351. let text = '自动调节视图';
  352. Switch.setSwitch(container, id_, onchange, text);
  353. executing = false;
  354. }
  355. }
  356. };
  357.  
  358. const Copy = {
  359. copy: function(value) {
  360. let textarea = document.getElementById('leetcode-assistant-textarea');
  361. textarea.value = value;
  362. textarea.setAttribute('value', value);
  363. textarea.select();
  364. document.execCommand('copy');
  365. bm.message({
  366. type: 'success',
  367. message: '复制成功',
  368. duration: 1500,
  369. });
  370. },
  371. copyClassStruct: function() {
  372. // 检查语言是否支持
  373. let lang = Get.getLanguage();
  374. if (!config.__supportLanguage.includes(lang)) {
  375. bm.message({
  376. type: 'warning',
  377. message: '目前不支持该语言的结构类代码复制',
  378. duration: 1500,
  379. });
  380. executing = false;
  381. return;
  382. }
  383. // 主要代码
  384. let sp = new StatementParser(lang);
  385. let classStructCode = sp.getClassStructFromCode(Get.getCode());
  386. if (!classStructCode) {
  387. bm.message({
  388. type: 'warning',
  389. message: '结构类代码不存在',
  390. duration: 1500,
  391. });
  392. return;
  393. }
  394. Copy.copy(classStructCode);
  395. },
  396. copyExampleInput: function(example) {
  397. // 检查语言是否支持
  398. let lang = Get.getLanguage();
  399. if (!config.__supportLanguage.includes(lang)) {
  400. bm.message({
  401. type: 'warning',
  402. message: '目前不支持该语言的示例输入代码复制',
  403. duration: 1500,
  404. });
  405. executing = false;
  406. return;
  407. }
  408. let sp = new StatementParser(lang);
  409. // 获取 declares
  410. let declares = sp.getDeclaresFromCode(Get.getCode());
  411. // 获取 expressions
  412. let inputTextElement = example.querySelector('strong').nextSibling;
  413. let inputText = '';
  414. while ((inputTextElement instanceof Text) || inputTextElement.tagName !== 'STRONG') {
  415. if (inputTextElement instanceof Text) {
  416. inputText += inputTextElement.wholeText;
  417. } else {
  418. inputText += inputTextElement.innerText;
  419. }
  420. inputTextElement = inputTextElement.nextSibling;
  421. }
  422. let expressions = inputText.trim().replace(/,$/, '').split(/,\s+/);
  423. // 生成语句并复制
  424. Copy.copy(sp.getStatementsFromDeclaresAndExpressions(declares, expressions));
  425. },
  426. };
  427.  
  428. const XHR = {
  429. requestCode: function(qid) {
  430. let query = `
  431. query mySubmissionDetail($id: ID!) {
  432. submissionDetail(submissionId: $id) {
  433. id
  434. code
  435. runtime
  436. memory
  437. rawMemory
  438. statusDisplay
  439. timestamp
  440. lang
  441. passedTestCaseCnt
  442. totalTestCaseCnt
  443. sourceUrl
  444. question {
  445. titleSlug
  446. title
  447. translatedTitle
  448. questionId
  449. __typename
  450. }
  451. ... on GeneralSubmissionNode {
  452. outputDetail {
  453. codeOutput
  454. expectedOutput
  455. input
  456. compileError
  457. runtimeError
  458. lastTestcase
  459. __typename
  460. }
  461. __typename
  462. }
  463. submissionComment {
  464. comment
  465. flagType
  466. __typename
  467. }
  468. __typename
  469. }
  470. }`;
  471. $.ajax({
  472. url: 'https://leetcode-cn.com/graphql/',
  473. method: 'POST',
  474. contentType: 'application/json',
  475. data: JSON.stringify({
  476. operationName: 'mySubmissionDetail',
  477. query: query,
  478. variables: {
  479. id: qid
  480. },
  481. }),
  482. }).then(res => {
  483. Copy.copy(res.data.submissionDetail.code);
  484. });
  485. }
  486. };
  487.  
  488. const Get = {
  489. getLanguage: function() {
  490. return document.getElementById('lang-select').innerText;
  491. },
  492. getCode: function() {
  493. return document.querySelector('[name=code]').value;
  494. }
  495. };
  496.  
  497. const Fun = {
  498. adjustViewScale: function(left, right) {
  499. if (!config.autoAdjustView) {
  500. executing = false;
  501. return;
  502. }
  503. let splitLine = document.querySelector('[data-is-collapsed]');
  504. let leftPart = splitLine.previousElementSibling;
  505. let rightPart = splitLine.nextElementSibling;
  506. let leftPartFlex = leftPart.style.flex.match(/\d+\.\d+/)[0];
  507. let rightPartFlex = rightPart.style.flex.match(/\d+\.\d+/)[0];
  508. leftPart.style.flex = leftPart.style.flex.replace(leftPartFlex, `${left}`);
  509. rightPart.style.flex = rightPart.style.flex.replace(rightPartFlex, `${right}`);
  510. executing = false;
  511. },
  512. highlightBestAcceptSubmission: function() {
  513. let highlightClassName = 'leetcode-assistant-highlight-accept-submission';
  514. let items = [...document.querySelectorAll('tr[data-row-key]')];
  515. let acItems = items.filter(item => item.querySelector('a[class^=ac]'));
  516. if (acItems.length === 0)
  517. return;
  518. let matchTimeMem = acItems.map(item => item.innerText.match(/(\d+)\sms.+?(\d+\.?\d)\sMB/).slice(1, 3));
  519. let timeList = matchTimeMem.map(res => parseInt(res[0]));
  520. let memList = matchTimeMem.map(res => parseFloat(res[1]));
  521. let targetIndex = 0;
  522. for (let i = 0; i < items.length; i++) {
  523. if (timeList[i] < timeList[targetIndex] || (timeList[i] === timeList[targetIndex] && memList[i] < memList[targetIndex])) {
  524. targetIndex = i;
  525. }
  526. }
  527. let lastTarget = document.querySelector(`.${highlightClassName}`);
  528. if (lastTarget)
  529. lastTarget.classList.remove(highlightClassName);
  530. acItems[targetIndex].classList.add(highlightClassName);
  531. }
  532. };
  533.  
  534. function main() {
  535. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\//)) { // /problems/*
  536. Basic.executeUtil(() => {
  537. Insert.copy.insertCopyStructCode();
  538. Insert.switch.insertAutoAdjustViewSwitch();
  539. }, () => {
  540. return document.querySelector('[class^=first-section-container]');
  541. });
  542. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\/$/)) { // 题目描述
  543. Fun.adjustViewScale(0.618, 0.382);
  544. Basic.executeUtil(Insert.copy.insertCopyExampleInput, () => {
  545. let codeDOM = document.querySelector('.editor-scrollable');
  546. let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
  547. return codeDOM && content && content.querySelector('pre');
  548. });
  549. } else if (location.href.includes('/solution/')) { // 题解
  550. Fun.adjustViewScale(0.382, 0.618);
  551. } else if (location.href.includes('/submissions/')) { // 提交记录
  552. Basic.executeUtil(() => {
  553. Insert.copy.insertCopySubmissionCode();
  554. Insert.copy.insertCopyTestInput();
  555. }, () => {
  556. return document.querySelector('.ant-table-thead');
  557. });
  558. }
  559. } else if (location.href.startsWith('https://leetcode-cn.com/problemset/')) { // 首页
  560. Insert.switch.insertRecommendVisibleSwitch();
  561. Switch.switchRecommendVisible();
  562. Basic.executeUtil(() => {
  563. Insert.switch.insertHideAnsweredQuestionSwitch();
  564. Switch.switchAnsweredQuestionVisible();
  565. }, () => {
  566. let navigation = document.querySelector('[role=navigation]');
  567. return navigation && navigation.innerText.length > 0;
  568. });
  569. } else {
  570. executing = false;
  571. }
  572. }
  573.  
  574. window.addEventListener('load', () => {
  575. Basic.updateData();
  576. Insert.base.insertStyle();
  577. Insert.base.insertTextarea();
  578. Basic.listenHistoryState();
  579. main();
  580. });
  581. })();