LeetCode Assistant

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

Verze ze dne 21. 09. 2021. Zobrazit nejnovější verzi.

  1. // ==UserScript==
  2. // @name LeetCode Assistant
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.4
  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 inputTextElement = example.querySelector('strong').nextSibling;
  416. let inputText = '';
  417. while ((inputTextElement instanceof Text) || !['STRONG', 'B'].includes(inputTextElement.tagName)) {
  418. if (inputTextElement instanceof Text) {
  419. inputText += inputTextElement.wholeText;
  420. } else {
  421. inputText += inputTextElement.innerText;
  422. }
  423. inputTextElement = inputTextElement.nextSibling;
  424. }
  425. let expressions = inputText.trim().replace(/,$/, '').split(/,\s+/);
  426. // 生成语句并复制
  427. Copy.copy(sp.getStatementsFromDeclaresAndExpressions(declares, expressions));
  428. },
  429. };
  430.  
  431. const XHR = {
  432. requestCode: function(qid) {
  433. let query = `
  434. query mySubmissionDetail($id: ID!) {
  435. submissionDetail(submissionId: $id) {
  436. id
  437. code
  438. runtime
  439. memory
  440. rawMemory
  441. statusDisplay
  442. timestamp
  443. lang
  444. passedTestCaseCnt
  445. totalTestCaseCnt
  446. sourceUrl
  447. question {
  448. titleSlug
  449. title
  450. translatedTitle
  451. questionId
  452. __typename
  453. }
  454. ... on GeneralSubmissionNode {
  455. outputDetail {
  456. codeOutput
  457. expectedOutput
  458. input
  459. compileError
  460. runtimeError
  461. lastTestcase
  462. __typename
  463. }
  464. __typename
  465. }
  466. submissionComment {
  467. comment
  468. flagType
  469. __typename
  470. }
  471. __typename
  472. }
  473. }`;
  474. $.ajax({
  475. url: 'https://leetcode-cn.com/graphql/',
  476. method: 'POST',
  477. contentType: 'application/json',
  478. data: JSON.stringify({
  479. operationName: 'mySubmissionDetail',
  480. query: query,
  481. variables: {
  482. id: qid
  483. },
  484. }),
  485. }).then(res => {
  486. Copy.copy(res.data.submissionDetail.code);
  487. });
  488. }
  489. };
  490.  
  491. const Get = {
  492. getLanguage: function() {
  493. return document.getElementById('lang-select').innerText;
  494. },
  495. getCode: function() {
  496. return document.querySelector('[name=code]').value;
  497. }
  498. };
  499.  
  500. const Fun = {
  501. adjustViewScale: function(left, right) {
  502. if (!config.autoAdjustView) {
  503. executing = false;
  504. return;
  505. }
  506. let splitLine = document.querySelector('[data-is-collapsed]');
  507. let leftPart = splitLine.previousElementSibling;
  508. let rightPart = splitLine.nextElementSibling;
  509. let leftPartFlex = leftPart.style.flex.match(/\d+\.\d+/)[0];
  510. let rightPartFlex = rightPart.style.flex.match(/\d+\.\d+/)[0];
  511. leftPart.style.flex = leftPart.style.flex.replace(leftPartFlex, `${left}`);
  512. rightPart.style.flex = rightPart.style.flex.replace(rightPartFlex, `${right}`);
  513. executing = false;
  514. },
  515. highlightBestAcceptSubmission: function() {
  516. let highlightClassName = 'leetcode-assistant-highlight-accept-submission';
  517. let items = [...document.querySelectorAll('tr[data-row-key]')];
  518. let acItems = items.filter(item => item.querySelector('a[class^=ac]'));
  519. if (acItems.length === 0)
  520. return;
  521. let matchTimeMem = acItems.map(item => item.innerText.match(/(\d+)\sms.+?(\d+\.?\d)\sMB/).slice(1, 3));
  522. let timeList = matchTimeMem.map(res => parseInt(res[0]));
  523. let memList = matchTimeMem.map(res => parseFloat(res[1]));
  524. let targetIndex = 0;
  525. for (let i = 0; i < items.length; i++) {
  526. if (timeList[i] < timeList[targetIndex] || (timeList[i] === timeList[targetIndex] && memList[i] < memList[targetIndex])) {
  527. targetIndex = i;
  528. }
  529. }
  530. let lastTarget = document.querySelector(`.${highlightClassName}`);
  531. if (lastTarget)
  532. lastTarget.classList.remove(highlightClassName);
  533. acItems[targetIndex].classList.add(highlightClassName);
  534. }
  535. };
  536.  
  537. function main() {
  538. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\//)) { // /problems/*
  539. Basic.executeUtil(() => {
  540. Insert.copy.insertCopyStructCode();
  541. Insert.switch.insertAutoAdjustViewSwitch();
  542. }, () => {
  543. return document.querySelector('[class^=first-section-container]');
  544. });
  545. if (location.href.match(/\/problems\/[a-zA-Z0-9\-]+\/$/)) { // 题目描述
  546. Fun.adjustViewScale(0.618, 0.382);
  547. Basic.executeUtil(Insert.copy.insertCopyExampleInput, () => {
  548. let codeDOM = document.querySelector('.editor-scrollable');
  549. let content = document.querySelector('[data-key=description-content] [class^=content] .notranslate');
  550. return codeDOM && content && content.querySelector('pre');
  551. });
  552. } else if (location.href.includes('/solution/')) { // 题解
  553. Fun.adjustViewScale(0.382, 0.618);
  554. } else if (location.href.includes('/submissions/')) { // 提交记录
  555. Basic.executeUtil(() => {
  556. Insert.copy.insertCopySubmissionCode();
  557. Insert.copy.insertCopyTestInput();
  558. }, () => {
  559. return document.querySelector('.ant-table-thead');
  560. });
  561. }
  562. } else if (location.href.startsWith('https://leetcode-cn.com/problemset/')) { // 首页
  563. Insert.switch.insertRecommendVisibleSwitch();
  564. Switch.switchRecommendVisible();
  565. Basic.executeUtil(() => {
  566. Insert.switch.insertHideAnsweredQuestionSwitch();
  567. Switch.switchAnsweredQuestionVisible();
  568. }, () => {
  569. let navigation = document.querySelector('[role=navigation]');
  570. return navigation && navigation.innerText.length > 0;
  571. });
  572. } else {
  573. executing = false;
  574. }
  575. }
  576.  
  577. window.addEventListener('load', () => {
  578. Basic.updateData();
  579. Insert.base.insertStyle();
  580. Insert.base.insertTextarea();
  581. Basic.listenHistoryState();
  582. main();
  583. });
  584. })();