LeetCode Assistant

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

As of 2021-10-01. See the latest version.

  1. // ==UserScript==
  2. // @name LeetCode Assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.6
  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]')];
  156. let matchPage = location.href.match(/\?page=(\d+)/);
  157. if (!matchPage || parseInt(matchPage[1]) === 1)
  158. nodes = nodes.slice(1);
  159. nodes = nodes.filter(node => node.querySelector('svg.text-green-s'));
  160. Switch.switchVisible(nodes, !config.__hideAnsweredQuestion, 'flex');
  161. }
  162. };
  163.  
  164. const Insert = {
  165. base: {
  166. insertStyle: function() {
  167. if (document.getElementById('leetcode-assistant-style'))
  168. return;
  169. let style = document.createElement('style');
  170. style.setAttribute('id', 'leetcode-assistant-style');
  171. style.innerText = `
  172. .leetcode-assistant-copy-example-button {
  173. border: 1px solid;
  174. border-radius: 2px;
  175. cursor: pointer;
  176. padding: 1px 4px;
  177. font-size: 0.8em;
  178. margin-top: 5px;
  179. width: fit-content;
  180. }
  181. .leetcode-assistant-highlight-accept-submission {
  182. font-weight: bold;
  183. }`;
  184. document.body.appendChild(style);
  185. },
  186. insertTextarea: function() {
  187. let textarea = document.createElement('textarea');
  188. textarea.setAttribute('id', 'leetcode-assistant-textarea');
  189. textarea.setAttribute('style', 'width: 0; height: 0;')
  190. document.body.appendChild(textarea);
  191. }
  192. },
  193. copy: {
  194. insertCopyStructCode: function() {
  195. const id_ = 'leetcode-assistant-copy-struct-button';
  196. if (document.getElementById(id_)) {
  197. executing = false;
  198. return;
  199. }
  200. let buttonContainer = document.querySelector('[class^=first-section-container]');
  201. let ref = buttonContainer.querySelector('button:nth-child(2)');
  202. let button = document.createElement('button');
  203. button.setAttribute('id', id_);
  204. button.className = ref.className;
  205. let span = document.createElement('span');
  206. span.className = ref.lastElementChild.className;
  207. span.innerText = '复制结构';
  208. button.appendChild(span);
  209. button.addEventListener('click', Copy.copyClassStruct);
  210. buttonContainer.appendChild(button);
  211. executing = false;
  212. },
  213. insertCopySubmissionCode: function() {
  214. let tbody = document.querySelector('.ant-table-tbody');
  215. let trs = [...tbody.querySelectorAll('tr')];
  216. let processTr = (tr) => {
  217. let qid = tr.dataset.rowKey;
  218. Basic.executeUtil((tr) => {
  219. let cell = tr.querySelector(':nth-child(4)');
  220. cell.title = '点击复制代码';
  221. cell.style = 'cursor: pointer; color: #007aff';
  222. cell.addEventListener('click', function() {
  223. XHR.requestCode(qid);
  224. });
  225. cell.setAttribute('data-set-copy', 'true');
  226. }, () => {
  227. let cell = tr.querySelector(':nth-child(4)');
  228. return cell && cell.dataset.setCopy !== 'true';
  229. }, [tr]);
  230. }
  231. trs.forEach(processTr);
  232. Fun.highlightBestAcceptSubmission();
  233. Basic.observeChildList(tbody, (nodes) => {
  234. let node = nodes[0];
  235. if (node.tagName === 'TR') {
  236. processTr(node);
  237. Fun.highlightBestAcceptSubmission();
  238. }
  239. });
  240. executing = false;
  241. },
  242. insertCopyExampleInput: function() {
  243. // 检查是否添加 "复制示例代码" 按钮
  244. let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
  245. if (content.dataset.addedCopyExampleInputButton === 'true')
  246. return;
  247. // 对每个 example 添加复制按钮
  248. let examples = [...content.querySelectorAll('pre')];
  249. for (let example of examples) {
  250. let btn = document.createElement('div');
  251. btn.innerText = '复制示例输入';
  252. btn.className = 'leetcode-assistant-copy-example-button';
  253. btn.addEventListener('click', () => {
  254. Copy.copyExampleInput(example);
  255. });
  256. example.appendChild(btn);
  257. }
  258. content.setAttribute('data-added-copy-example-input-button', 'true');
  259. executing = false;
  260. },
  261. insertCopyTestInput: function() {
  262. function addCopyTestInputForInputInfo(inputInfo) {
  263. inputInfo = inputInfo || document.querySelector('[class^=result-container] [class*=ValueContainer]');
  264. if (inputInfo && inputInfo.dataset.setCopy !== 'true') {
  265. inputInfo.addEventListener('click', function() {
  266. // 检查是否支持语言
  267. let lang = Get.getLanguage();
  268. if (!config.__supportLanguage.includes(lang)) {
  269. bm.message({
  270. type: 'warning',
  271. message: '目前不支持该语言的测试输入代码复制',
  272. duration: 1500,
  273. });
  274. executing = false;
  275. return;
  276. }
  277. // 主要代码
  278. let sp = new StatementParser(lang);
  279. let expressions = this.innerText.trim().split('\n');
  280. let declares = sp.getDeclaresFromCode(Get.getCode());
  281. let statements = sp.getStatementsFromDeclaresAndExpressions(declares, expressions);
  282. Copy.copy(statements);
  283. });
  284. inputInfo.setAttribute('data-set-copy', 'true');
  285. }
  286. }
  287. let submissions = document.querySelector('[class^=submissions]');
  288. submissions.addEventListener('DOMNodeInserted', function(event) {
  289. if (event.target.className.startsWith('container') || event.target.className.includes('Container')) {
  290. Basic.executeUtil((container) => {
  291. let inputInfo = container.querySelector('[class*=ValueContainer]');
  292. addCopyTestInputForInputInfo(inputInfo);
  293. }, () => {
  294. return event.target.querySelector('[class*=ValueContainer]');
  295. }, [event.target]);
  296. }
  297. });
  298. addCopyTestInputForInputInfo();
  299. executing = false;
  300. },
  301. },
  302. switch: {
  303. insertRecommendVisibleSwitch: function() {
  304. const id_ = 'leetcode-assistant-recommend-visible-switch';
  305. if (document.getElementById(id_)) {
  306. executing = false;
  307. return;
  308. }
  309. let container = document.querySelector('.relative.space-x-5').nextElementSibling;
  310. let onchange = function() {
  311. Basic.updateData({ recommendVisible: !this.checked });
  312. Switch.switchRecommendVisible();
  313. };
  314. let text = '简洁模式';
  315. Switch.setSwitch(container, id_, onchange, text);
  316. executing = false;
  317. },
  318. insertHideAnsweredQuestionSwitch: function() {
  319. const id_ = 'leetcode-assistant-hide-answered-question-switch';
  320. if (document.getElementById(id_)) {
  321. executing = false;
  322. return;
  323. }
  324. let container = document.createElement('div');
  325. document.querySelector('.relative.space-x-5').parentElement.appendChild(container);
  326. let onchange = function() {
  327. config.__hideAnsweredQuestion = !config.__hideAnsweredQuestion;
  328. Switch.switchAnsweredQuestionVisible();
  329. };
  330. let text = '隐藏已解决';
  331. Switch.setSwitch(container, id_, onchange, text, false);
  332. let navigation = document.querySelector('[role=navigation]');
  333. let btns = [...navigation.querySelectorAll('button')];
  334. btns.forEach(btn => {
  335. btn.addEventListener("click", function() {
  336. document.getElementById(id_).checked = false;
  337. config.__hideAnsweredQuestion = false;
  338. Switch.switchAnsweredQuestionVisible();
  339. return true;
  340. });
  341. });
  342. executing = false;
  343. },
  344. insertAutoAdjustViewSwitch: function() {
  345. const id_ = 'leetcode-assistant-auto-adjust-view-switch';
  346. if (document.getElementById(id_)) {
  347. executing = false;
  348. return;
  349. }
  350. let container = document.querySelector('[data-status] nav > ul');
  351. let onchange = function() {
  352. Basic.updateData({ autoAdjustView: this.checked });
  353. };
  354. let text = '自动调节视图';
  355. Switch.setSwitch(container, id_, onchange, text);
  356. executing = false;
  357. }
  358. }
  359. };
  360.  
  361. const Copy = {
  362. copy: function(value) {
  363. let textarea = document.getElementById('leetcode-assistant-textarea');
  364. textarea.value = value;
  365. textarea.setAttribute('value', value);
  366. textarea.select();
  367. document.execCommand('copy');
  368. bm.message({
  369. type: 'success',
  370. message: '复制成功',
  371. duration: 1500,
  372. });
  373. },
  374. copyClassStruct: function() {
  375. // 检查语言是否支持
  376. let lang = Get.getLanguage();
  377. if (!config.__supportLanguage.includes(lang)) {
  378. bm.message({
  379. type: 'warning',
  380. message: '目前不支持该语言的结构类代码复制',
  381. duration: 1500,
  382. });
  383. executing = false;
  384. return;
  385. }
  386. // 主要代码
  387. let sp = new StatementParser(lang);
  388. let classStructCode = sp.getClassStructFromCode(Get.getCode());
  389. if (!classStructCode) {
  390. bm.message({
  391. type: 'warning',
  392. message: '结构类代码不存在',
  393. duration: 1500,
  394. });
  395. return;
  396. }
  397. Copy.copy(classStructCode);
  398. },
  399. copyExampleInput: function(example) {
  400. // 检查语言是否支持
  401. let lang = Get.getLanguage();
  402. if (!config.__supportLanguage.includes(lang)) {
  403. bm.message({
  404. type: 'warning',
  405. message: '目前不支持该语言的示例输入代码复制',
  406. duration: 1500,
  407. });
  408. executing = false;
  409. return;
  410. }
  411. let sp = new StatementParser(lang);
  412. // 获取 declares
  413. let declares = sp.getDeclaresFromCode(Get.getCode());
  414. // 获取 expressions
  415. let strong = example.querySelector('strong');
  416. let inputText = "";
  417. if (strong && strong.nextSibling) {
  418. let inputTextElement = strong.nextSibling;
  419. while ((inputTextElement instanceof Text) || !['STRONG', 'B'].includes(inputTextElement.tagName)) {
  420. if (inputTextElement instanceof Text) {
  421. inputText += inputTextElement.wholeText;
  422. } else {
  423. inputText += inputTextElement.innerText;
  424. }
  425. inputTextElement = inputTextElement.nextSibling;
  426. }
  427. } else {
  428. inputText = example.innerText.replace(/\n/g, '').match(/输入:(.+)输出:/)[1];
  429. }
  430. let expressions = inputText.trim().replace(/,$/, '');
  431. if (inputText.replace(/".+?"/g, '').includes(',')) {
  432. // 无视字符串后存在逗号分隔符,说明有多个输入
  433. expressions = expressions.split(/,\s+/);
  434. } else {
  435. // 单个输入
  436. expressions = [expressions];
  437. }
  438. // 生成语句并复制
  439. Copy.copy(sp.getStatementsFromDeclaresAndExpressions(declares, expressions));
  440. },
  441. };
  442.  
  443. const XHR = {
  444. requestCode: function(qid) {
  445. let query = `
  446. query mySubmissionDetail($id: ID!) {
  447. submissionDetail(submissionId: $id) {
  448. id
  449. code
  450. runtime
  451. memory
  452. rawMemory
  453. statusDisplay
  454. timestamp
  455. lang
  456. passedTestCaseCnt
  457. totalTestCaseCnt
  458. sourceUrl
  459. question {
  460. titleSlug
  461. title
  462. translatedTitle
  463. questionId
  464. __typename
  465. }
  466. ... on GeneralSubmissionNode {
  467. outputDetail {
  468. codeOutput
  469. expectedOutput
  470. input
  471. compileError
  472. runtimeError
  473. lastTestcase
  474. __typename
  475. }
  476. __typename
  477. }
  478. submissionComment {
  479. comment
  480. flagType
  481. __typename
  482. }
  483. __typename
  484. }
  485. }`;
  486. $.ajax({
  487. url: 'https://leetcode-cn.com/graphql/',
  488. method: 'POST',
  489. contentType: 'application/json',
  490. data: JSON.stringify({
  491. operationName: 'mySubmissionDetail',
  492. query: query,
  493. variables: {
  494. id: qid
  495. },
  496. }),
  497. }).then(res => {
  498. Copy.copy(res.data.submissionDetail.code);
  499. });
  500. }
  501. };
  502.  
  503. const Get = {
  504. getLanguage: function() {
  505. return document.getElementById('lang-select').innerText;
  506. },
  507. getCode: function() {
  508. return document.querySelector('[name=code]').value;
  509. }
  510. };
  511.  
  512. const Fun = {
  513. adjustViewScale: function(left, right) {
  514. if (!config.autoAdjustView) {
  515. executing = false;
  516. return;
  517. }
  518. let splitLine = document.querySelector('[data-is-collapsed]');
  519. let leftPart = splitLine.previousElementSibling;
  520. let rightPart = splitLine.nextElementSibling;
  521. let leftPartFlex = leftPart.style.flex.match(/\d+\.\d+/)[0];
  522. let rightPartFlex = rightPart.style.flex.match(/\d+\.\d+/)[0];
  523. leftPart.style.flex = leftPart.style.flex.replace(leftPartFlex, `${left}`);
  524. rightPart.style.flex = rightPart.style.flex.replace(rightPartFlex, `${right}`);
  525. executing = false;
  526. },
  527. highlightBestAcceptSubmission: function() {
  528. let highlightClassName = 'leetcode-assistant-highlight-accept-submission';
  529. let items = [...document.querySelectorAll('tr[data-row-key]')];
  530. let acItems = items.filter(item => item.querySelector('a[class^=ac]'));
  531. if (acItems.length === 0)
  532. return;
  533. let matchTimeMem = acItems.map(item => item.innerText.match(/(\d+)\sms.+?(\d+\.?\d)\sMB/).slice(1, 3));
  534. let timeList = matchTimeMem.map(res => parseInt(res[0]));
  535. let memList = matchTimeMem.map(res => parseFloat(res[1]));
  536. let targetIndex = 0;
  537. for (let i = 0; i < items.length; i++) {
  538. if (timeList[i] < timeList[targetIndex] || (timeList[i] === timeList[targetIndex] && memList[i] < memList[targetIndex])) {
  539. targetIndex = i;
  540. }
  541. }
  542. let lastTarget = document.querySelector(`.${highlightClassName}`);
  543. if (lastTarget)
  544. lastTarget.classList.remove(highlightClassName);
  545. acItems[targetIndex].classList.add(highlightClassName);
  546. }
  547. };
  548.  
  549. function main() {
  550. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\//)) { // /problems/*
  551. Basic.executeUtil(() => {
  552. Insert.copy.insertCopyStructCode();
  553. Insert.switch.insertAutoAdjustViewSwitch();
  554. }, () => {
  555. return document.querySelector('[class^=first-section-container]');
  556. });
  557. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\/$/)) { // 题目描述
  558. Fun.adjustViewScale(0.618, 0.382);
  559. Basic.executeUtil(Insert.copy.insertCopyExampleInput, () => {
  560. let codeDOM = document.querySelector('.editor-scrollable');
  561. let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
  562. return codeDOM && content && content.querySelector('pre');
  563. });
  564. } else if (location.href.includes('/solution/')) { // 题解
  565. Fun.adjustViewScale(0.382, 0.618);
  566. } else if (location.href.includes('/submissions/')) { // 提交记录
  567. Basic.executeUtil(() => {
  568. Insert.copy.insertCopySubmissionCode();
  569. Insert.copy.insertCopyTestInput();
  570. }, () => {
  571. return document.querySelector('.ant-table-thead');
  572. });
  573. }
  574. } else if (location.href.startsWith('https://leetcode-cn.com/problemset/')) { // 首页
  575. Insert.switch.insertRecommendVisibleSwitch();
  576. Switch.switchRecommendVisible();
  577. Basic.executeUtil(() => {
  578. Insert.switch.insertHideAnsweredQuestionSwitch();
  579. Switch.switchAnsweredQuestionVisible();
  580. }, () => {
  581. let navigation = document.querySelector('[role=navigation]');
  582. return navigation && navigation.innerText.length > 0;
  583. });
  584. } else {
  585. executing = false;
  586. }
  587. }
  588.  
  589. window.addEventListener('load', () => {
  590. Basic.updateData();
  591. Insert.base.insertStyle();
  592. Insert.base.insertTextarea();
  593. Basic.listenHistoryState();
  594. main();
  595. });
  596. })();