LeetCode Helper for JavaScript

try to take over the world!

  1. // ==UserScript==
  2. // @name LeetCode Helper for JavaScript
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.10
  5. // @description try to take over the world!
  6. // @author You
  7. // @match https://leetcode-cn.com/problems/*
  8. // @match https://leetcode-cn.com/contest/*/problems/*
  9. // @match https://leetcode.cn/problems/*
  10. // @match https://leetcode.cn/contest/*/problems/*
  11. // @match https://leetcode.com/problems/*
  12. // @match https://leetcode.com/contest/*/problems/*
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. console.log('loading leetcode helper');
  20. const STYLE = `
  21. .leetcode-helper {
  22. position: fixed;
  23. background: rgba(255,255,255,1);
  24. z-index: 1024;
  25. width: 400px;
  26. min-height: 50px;
  27. top: 0;
  28. left: 0;
  29. border: .5px solid rgb(255, 109, 0);
  30. max-height: 500px;
  31. overflow-y: auto;
  32. }
  33. .leetcode-helper-header {
  34. font-weight: bold;
  35. color: white;
  36. background: rgb(255, 164, 97);
  37. padding: 2px 7px;
  38. position: sticky;
  39. top: 0;
  40. }
  41. .leetcode-helper-body {
  42. padding: 5px 10px;
  43. }
  44. .leetcode-helper-body .case {
  45. display: flex;
  46. justify-content: space-between;
  47. }
  48. .leetcode-helper-body .case>div {
  49. flex: 0 1 30%;
  50. overflow: auto;
  51. }
  52. .leetcode-helper-body section>div:last-child {
  53. flex: 0 1 60%;
  54. }
  55. .leetcode-helper-body section p {
  56. margin-bottom: 0px;
  57. }
  58. .leetcode-helper-body label {
  59. color: rgb(255, 109, 0); margin-right: 5px;'
  60. }
  61. .leetcode-helper-status {
  62. margin-left: 5px;
  63. }
  64. .leetcode-helper .case .title button {
  65. line-height: 12px;
  66. font-size: 12px;
  67. }
  68. .leetcode-helper .case textarea {
  69. width: 100%;
  70. overflow: auto;
  71. white-space: nowrap;
  72. border: 1px solid black;
  73. border-left: none;
  74. font-family: monospace;
  75. }
  76. .leetcode-helper .case div:first-child textarea {
  77. border-left: 1px solid black;
  78. }
  79. .leetcode-helper .success {
  80. background-color: lightgreen;
  81. }
  82. .leetcode-helper .error {
  83. background-color: #ff9090;
  84. }
  85. .leetcode-helper .message {
  86. white-space: pre;
  87. font-family: monospace;
  88. line-height: 1.2;
  89. padding: 2px 5px;
  90. max-height: 20em;
  91. overflow: auto;
  92. }
  93. .leetcode-helper .operations {
  94. margin-top: 5px;
  95. }
  96. `
  97.  
  98. main();
  99.  
  100. async function main() {
  101. insertStyleSheets();
  102. const panel = createPanel()
  103. console.log('panel created:', panel);
  104. setDraggable(panel.querySelector('.leetcode-helper-header'), panel);
  105. document.body.appendChild(panel);
  106. }
  107.  
  108. function getEditorText() {
  109. if (typeof monaco !== 'undefined') { // window is not the window, so window.monaco wont't work
  110. return monaco.editor.getModels()[0].getValue()
  111. }
  112. const el1 = document.querySelector('.editor-scrollable')
  113. if (el1) return el1.innerText
  114.  
  115. const el2 = document.querySelector('.CodeMirror')
  116. if (el2) return el2.CodeMirror.getValue()
  117.  
  118. return 'editor not found'
  119. }
  120. function getResolver(log) {
  121. const body = getEditorText()
  122. const match = /var\s+([a-zA-Z_$][\w$]*)\s*=/.exec(body)
  123. if (!match) throw new Error('resolver var xxx = function(){} not found')
  124. const fn = new Function(`console`, `${body}\n return ${match[1]}`)
  125. return fn({
  126. log: function(...args) {
  127. log(args.map(serialize).join(' '))
  128. },
  129. error: function(...args) {
  130. log(args.map(serialize).join(' '))
  131. }
  132. })
  133. }
  134. function lineOffset() {
  135. try {
  136. const fn = new Function('console', 'throw new Error(314)')
  137. fn()
  138. } catch(e){
  139. const match = /(\d+):\d+\)($|\n)/.exec(e.stack)
  140. return match ? +match[1] - 1 : 2
  141. }
  142. }
  143.  
  144. function insertStyleSheets() {
  145. const style = document.createElement('style')
  146. style.innerHTML = STYLE
  147. document.body.appendChild(style)
  148. }
  149.  
  150. function getDescription() {
  151. return new Promise((resolve, reject) => {
  152. const interval = setInterval(() => {
  153. const el1 = document.querySelector('[data-key=description-content]')
  154. const el2 = document.querySelector('.question-content')
  155. const content = el1 && el1.innerText || el2 && el2.innerText
  156. if (!content) return
  157. clearInterval(interval)
  158. resolve(content)
  159. }, 300);
  160. })
  161. }
  162.  
  163. function setDraggable(handle, panel) {
  164. let dragging = false
  165. let initX
  166. let initY
  167. let initMarginX
  168. let initMarginY
  169. handle.addEventListener('mousedown', e => {
  170. dragging = true
  171. initX = e.clientX - (panel.style.left.slice(0, -2) || 0)
  172. initY = e.clientY - (panel.style.top.slice(0, -2) || 0)
  173. // console.log(mousedown, recording (${initX}, ${initY}))
  174. })
  175. window.addEventListener('mousemove', e => {
  176. if (!dragging) return
  177. const l = Math.min(window.innerWidth - 15, Math.max(0, e.clientX - initX))
  178. const t = Math.min(window.innerHeight - 15, Math.max(0, e.clientY - initY))
  179. // console.log(moving to (${l}, ${r}));
  180. panel.style.left = l + 'px'
  181. panel.style.top = t + 'px'
  182. })
  183. window.addEventListener('mouseup', e => {
  184. dragging = false
  185. GM_setValue('pos', [+panel.style.left.slice(0, -2), +panel.style.top.slice(0, -2)])
  186. })
  187. }
  188. function renderCases (ios, caseList) {
  189. for(const io of ios) {
  190. caseList.append(createCase(io.input, io.expected))
  191. }
  192. }
  193. function loadCases () {
  194. let ios
  195. try {
  196. ios = JSON.parse(GM_getValue('leetcode.io:' + location.href))
  197. } catch (err) {
  198. return false
  199. }
  200. if (!ios) return false
  201. return ios
  202. }
  203. async function saveCases () {
  204. const sections = document.querySelectorAll('.leetcode-helper .case-list .case')
  205. const ios = [...sections].map(section => ({
  206. input: section.querySelector('.input').value,
  207. expected: section.querySelector('.expected').value
  208. }))
  209. GM_setValue('leetcode.io:' + location.href, JSON.stringify(ios))
  210. console.log('cases saved', ios)
  211. }
  212. async function parseIO(caseList) {
  213. console.log('parsing IO from HTML...')
  214. const desc = await getDescription();
  215. const ios = parse(desc);
  216. console.log('parsed sample input/expected', ios);
  217. renderCases(ios, caseList);
  218. if (ios.length === 0) info('sample input/output not found')
  219. else saveCases(ios)
  220. }
  221. function createPanel() {
  222. const panel = document.createElement('div');
  223. panel.setAttribute('class', 'leetcode-helper');
  224. const pos = GM_getValue('pos')
  225. if (pos) {
  226. panel.style.left = Math.min(pos[0], window.innerWidth - 50) + 'px'
  227. panel.style.top = Math.min(pos[1], window.innerHeight - 50) + 'px'
  228. }
  229.  
  230. const header = document.createElement('div');
  231. header.innerText = 'LeetCode Helper';
  232. header.setAttribute('class', 'leetcode-helper-header');
  233. panel.appendChild(header);
  234.  
  235. const body = document.createElement('div');
  236. body.setAttribute('class', 'leetcode-helper-body');
  237. panel.appendChild(body);
  238.  
  239. const caseList = document.createElement('div')
  240. caseList.classList.add('case-list')
  241. body.appendChild(caseList)
  242.  
  243. window.messageEl = document.createElement('div')
  244. window.messageEl.classList.add('message')
  245. body.appendChild(window.messageEl);
  246.  
  247. const operations = document.createElement('div')
  248. operations.classList.add('operations')
  249. operations.appendChild(createButton('RunAll', x => runAll(caseList.querySelectorAll('.case'))))
  250. operations.appendChild(createButton('AddCase', () => caseList.append(createCase())))
  251. operations.appendChild(createButton('Refresh', () => {
  252. caseList.innerHTML = ''
  253. parseIO(caseList)
  254. }))
  255. body.appendChild(operations)
  256.  
  257. const ios = loadCases()
  258. if (ios) renderCases(ios, caseList);
  259. else parseIO(caseList);
  260. return panel;
  261. }
  262. function createCase(input = '', expected = '') {
  263. const section = document.createElement('section')
  264. section.classList.add('case')
  265. section.appendChild(createData('Input', input))
  266. section.appendChild(createData('Expected', expected))
  267.  
  268. const output = createData('Output', '')
  269. output.querySelector('.title').appendChild(createButton('Run', () => run(section)))
  270. output.querySelector('.title').appendChild(createButton('Delete', () => section.remove()))
  271. section.appendChild(output)
  272. return section
  273. }
  274. function run(section) {
  275. const input = section.querySelector('.input').value
  276. const expected = section.querySelector('.expected').value
  277. const outputEl = section.querySelector('.output')
  278. info('Running...', section)
  279.  
  280. requestAnimationFrame(() => requestAnimationFrame(() => {
  281. let args
  282. try {
  283. args = input.split('\n').map(parseArg)
  284. } catch (err) {
  285. outputEl.value = err.stack.split('\n').map(x => x.replace(/\([^:]*:[^:]*:/, '(')).join('\n')
  286. console.error(err)
  287. return error(outputEl.value, section)
  288. }
  289. console.log('calling resolver with', ...args)
  290.  
  291. clear(section)
  292. let result = null
  293. let resolver
  294. try {
  295. fn = getResolver(x => info(x, section))
  296. } catch (err) {
  297. outputEl.value = err.stack.split('\n').slice(0, -3).join('\n')
  298. console.error(err)
  299. return error(outputEl.value, section)
  300. }
  301. try {
  302. result = fn(...args)
  303. console.log('result:', result)
  304. } catch(err) {
  305. const offset = lineOffset();
  306. const fixLineNumber = line => line.replace(/(\d+):(\d+)\)$/, (match, line, col) => `${line - offset}:${col})`)
  307. outputEl.value = err.stack.split('\n').slice(0, -2).map(x => x.replace(/eval at [^)]*\), <[^>]*>:/, '')).map(fixLineNumber).join('\n')
  308. console.error(err)
  309. return error(outputEl.value, section)
  310. }
  311. const output = serialize(result)
  312. outputEl.value = output
  313. if (output === expected) {
  314. success('Accepted', section)
  315. } else {
  316. error('Wrong Answer', section)
  317. console.error(`Failed:\nExpected: ${expected}\nOutput: ${output}`)
  318. }
  319. }))
  320. }
  321. function runAll(sections) {
  322. for(const section of sections) run(section)
  323. }
  324. function clear(section) {
  325. const outputEl = section.querySelector('.output')
  326. outputEl.classList.remove('error')
  327. outputEl.classList.remove('success')
  328.  
  329. const messageEl = window.messageEl
  330. messageEl.innerText = ''
  331. messageEl.classList.remove('info')
  332. messageEl.classList.remove('error')
  333. messageEl.classList.remove('success')
  334. }
  335. function success(msg, section) {
  336. const outputEl = section.querySelector('.output')
  337. outputEl.classList.add('success')
  338.  
  339. const messageEl = window.messageEl
  340. messageEl.innerText += msg + '\n'
  341. messageEl.classList.add('success')
  342. }
  343. function info(msg, section) {
  344. console.log(msg)
  345. const messageEl = window.messageEl
  346. messageEl.innerText += msg + '\n'
  347. messageEl.classList.add('info')
  348. }
  349. function error(msg, section) {
  350. const outputEl = section.querySelector('.output')
  351. outputEl.classList.add('error')
  352.  
  353. const messageEl = window.messageEl
  354. messageEl.innerText += msg + '\n'
  355. messageEl.classList.add('error')
  356. }
  357. function serialize(result) {
  358. return JSON.stringify(result, (k, v) => {
  359. if (Number.isNaN(v) || v === Infinity || v === -Infinity || v === undefined || typeof v === 'bigint') return '' + v
  360. return v
  361. })
  362. }
  363. function parseArg(arg) {
  364. return JSON.parse(arg.trim())
  365. }
  366. function createButton(text, onClick) {
  367. const btn = document.createElement('button')
  368. btn.innerText = text
  369. btn.addEventListener('click', onClick)
  370. return btn
  371. }
  372. function createData(labelText, str = '') {
  373. const div = document.createElement('div');
  374.  
  375. const p = document.createElement('p')
  376. p.classList.add('title')
  377. const label = document.createElement('label')
  378. label.innerText = labelText
  379. p.appendChild(label);
  380. div.appendChild(p);
  381.  
  382. const textarea = document.createElement('textarea')
  383. textarea.setAttribute('class', labelText.toLowerCase())
  384. textarea.value = str;
  385. textarea.addEventListener('blur', () => saveCases())
  386. div.appendChild(textarea)
  387. return div
  388. }
  389. function parse(text) {
  390. const r = /(?:输入|Input)[::]([\s\S]*?)(?:输出|Output)[::][\s\n]*(?:.*:)?(.*)(\n|$)/ig
  391. const ans = []
  392. let match
  393. while(match = r.exec(text)) {
  394. const [, input, expected] = match
  395. ans.push({
  396. input: parseInput(input.trim()),
  397. expected: parseExpected(expected)
  398. })
  399. }
  400. return ans
  401. }
  402. function parseExpected(expected) {
  403. try {
  404. return JSON.stringify(JSON.parse(expected))
  405. } catch (err) {
  406. return expected
  407. }
  408. }
  409. function parseInput(input) {
  410. const args = []
  411. const pair = {
  412. "'": "'",
  413. '"': '"',
  414. '[': ']',
  415. '{': '}',
  416. '(': ')'
  417. }
  418. let state = 'input'
  419. let stack
  420. let arg
  421. for(let i = 0; i < input.length; i++) {
  422. const char = input.charAt(i)
  423. if (state === 'input') {
  424. if (char === '=') {
  425. state = 'expr'
  426. arg = ''
  427. stack = []
  428. }
  429. } else if (state === 'expr') {
  430. if ('"\'[]{}()'.includes(char) && input[i - 1] !== '\\') {
  431. if (pair[stack[stack.length - 1]] === char) stack.pop()
  432. else stack.push(char)
  433. arg += char
  434. } else if (stack.length) {
  435. arg += char
  436. } else if ((char === ',' || char === '\n') && stack.length === 0) {
  437. state = 'input'
  438. args.push(arg)
  439. arg = ''
  440. } else {
  441. arg += char
  442. }
  443. }
  444. }
  445. if (arg === undefined) args.push(input)
  446. else if (arg) args.push(arg)
  447. return args.map(x => x.split('\n').map(l => l.trim()).join(' ').trim()).join('\n')
  448. }
  449. })();