Greasy Fork is available in English.

LeetCode Helper for JavaScript

try to take over the world!

Per 29-04-2020. Zie de nieuwste versie.

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