Greasy Fork is available in English.

LeetCode Helper for JavaScript

try to take over the world!

06.09.2020 itibariyledir. En son verisyonu görün.

  1. // ==UserScript==
  2. // @name LeetCode Helper for JavaScript
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.8
  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. function lineOffset() {
  132. try {
  133. const fn = new Function('console', 'throw new Error(314)')
  134. fn()
  135. } catch(e){
  136. const match = /(\d+):\d+\)($|\n)/.exec(e.stack)
  137. return match ? +match[1] - 1 : 2
  138. }
  139. }
  140.  
  141. function insertStyleSheets() {
  142. const style = document.createElement('style')
  143. style.innerHTML = STYLE
  144. document.body.appendChild(style)
  145. }
  146.  
  147. function getDescription() {
  148. return new Promise((resolve, reject) => {
  149. const interval = setInterval(() => {
  150. const el1 = document.querySelector('[data-key=description-content]')
  151. const el2 = document.querySelector('.question-content')
  152. const content = el1 && el1.innerText || el2 && el2.innerText
  153. if (!content) return
  154. clearInterval(interval)
  155. resolve(content)
  156. }, 300);
  157. })
  158. }
  159.  
  160. function setDraggable(handle, panel) {
  161. let dragging = false
  162. let initX
  163. let initY
  164. let initMarginX
  165. let initMarginY
  166. handle.addEventListener('mousedown', e => {
  167. dragging = true
  168. initX = e.clientX - (panel.style.left.slice(0, -2) || 0)
  169. initY = e.clientY - (panel.style.top.slice(0, -2) || 0)
  170. // console.log(mousedown, recording (${initX}, ${initY}))
  171. })
  172. window.addEventListener('mousemove', e => {
  173. if (!dragging) return
  174. const l = Math.min(window.innerWidth - 15, Math.max(0, e.clientX - initX))
  175. const t = Math.min(window.innerHeight - 15, Math.max(0, e.clientY - initY))
  176. // console.log(moving to (${l}, ${r}));
  177. panel.style.left = l + 'px'
  178. panel.style.top = t + 'px'
  179. })
  180. window.addEventListener('mouseup', e => {
  181. dragging = false
  182. GM_setValue('pos', [+panel.style.left.slice(0, -2), +panel.style.top.slice(0, -2)])
  183. })
  184. }
  185. function renderCases (ios, caseList) {
  186. for(const io of ios) {
  187. caseList.append(createCase(io.input, io.expected))
  188. }
  189. }
  190. function loadCases () {
  191. let ios
  192. try {
  193. ios = JSON.parse(GM_getValue('leetcode.io:' + location.href))
  194. } catch (err) {
  195. return false
  196. }
  197. if (!ios) return false
  198. return ios
  199. }
  200. async function saveCases () {
  201. const sections = document.querySelectorAll('.leetcode-helper .case-list .case')
  202. const ios = [...sections].map(section => ({
  203. input: section.querySelector('.input').value,
  204. expected: section.querySelector('.expected').value
  205. }))
  206. GM_setValue('leetcode.io:' + location.href, JSON.stringify(ios))
  207. console.log('cases saved', ios)
  208. }
  209. async function parseIO(caseList) {
  210. console.log('parsing IO from HTML...')
  211. const desc = await getDescription();
  212. const ios = parse(desc);
  213. console.log('parsed sample input/expected', ios);
  214. renderCases(ios, caseList);
  215. if (ios.length === 0) info('sample input/output not found')
  216. else saveCases(ios)
  217. }
  218. function createPanel() {
  219. const panel = document.createElement('div');
  220. panel.setAttribute('class', 'leetcode-helper');
  221. const pos = GM_getValue('pos')
  222. if (pos) {
  223. panel.style.left = Math.min(pos[0], window.innerWidth - 50) + 'px'
  224. panel.style.top = Math.min(pos[1], window.innerHeight - 50) + 'px'
  225. }
  226.  
  227. const header = document.createElement('div');
  228. header.innerText = 'LeetCode Helper';
  229. header.setAttribute('class', 'leetcode-helper-header');
  230. panel.appendChild(header);
  231.  
  232. const body = document.createElement('div');
  233. body.setAttribute('class', 'leetcode-helper-body');
  234. panel.appendChild(body);
  235.  
  236. const caseList = document.createElement('div')
  237. caseList.classList.add('case-list')
  238. body.appendChild(caseList)
  239.  
  240. window.messageEl = document.createElement('div')
  241. window.messageEl.classList.add('message')
  242. body.appendChild(window.messageEl);
  243.  
  244. const operations = document.createElement('div')
  245. operations.classList.add('operations')
  246. operations.appendChild(createButton('RunAll', x => runAll(caseList.querySelectorAll('.case'))))
  247. operations.appendChild(createButton('AddCase', () => caseList.append(createCase())))
  248. operations.appendChild(createButton('Refresh', () => {
  249. caseList.innerHTML = ''
  250. parseIO(caseList)
  251. }))
  252. body.appendChild(operations)
  253.  
  254. const ios = loadCases()
  255. if (ios) renderCases(ios, caseList);
  256. else parseIO(caseList);
  257. return panel;
  258. }
  259. function createCase(input = '', expected = '') {
  260. const section = document.createElement('section')
  261. section.classList.add('case')
  262. section.appendChild(createData('Input', input))
  263. section.appendChild(createData('Expected', expected))
  264.  
  265. const output = createData('Output', '')
  266. output.querySelector('.title').appendChild(createButton('Run', () => run(section)))
  267. output.querySelector('.title').appendChild(createButton('Delete', () => section.remove()))
  268. section.appendChild(output)
  269. return section
  270. }
  271. function run(section) {
  272. const input = section.querySelector('.input').value
  273. const expected = section.querySelector('.expected').value
  274. const outputEl = section.querySelector('.output')
  275. info('Running...', section)
  276.  
  277. requestAnimationFrame(() => requestAnimationFrame(() => {
  278. let args
  279. try {
  280. args = input.split('\n').map(parseArg)
  281. } catch (err) {
  282. outputEl.value = err.stack.split('\n').map(x => x.replace(/\([^:]*:[^:]*:/, '(')).join('\n')
  283. console.error(err)
  284. return error(outputEl.value, section)
  285. }
  286. console.log('calling resolver with', ...args)
  287.  
  288. clear(section)
  289. let result = null
  290. let resolver
  291. try {
  292. fn = getResolver(x => info(x, section))
  293. } catch (err) {
  294. outputEl.value = err.stack.split('\n').slice(0, -3).join('\n')
  295. console.error(err)
  296. return error(outputEl.value, section)
  297. }
  298. try {
  299. result = fn(...args)
  300. console.log('result:', result)
  301. } catch(err) {
  302. const offset = lineOffset();
  303. const fixLineNumber = line => line.replace(/(\d+):(\d+)\)$/, (match, line, col) => `${line - offset}:${col})`)
  304. outputEl.value = err.stack.split('\n').slice(0, -2).map(x => x.replace(/eval at [^)]*\), <[^>]*>:/, '')).map(fixLineNumber).join('\n')
  305. console.error(err)
  306. return error(outputEl.value, section)
  307. }
  308. const output = serialize(result)
  309. outputEl.value = output
  310. if (output === expected) {
  311. success('Accepted', section)
  312. } else {
  313. error('Wrong Answer', section)
  314. console.error(`Failed:\nExpected: ${expected}\nOutput: ${output}`)
  315. }
  316. }))
  317. }
  318. function runAll(sections) {
  319. for(const section of sections) run(section)
  320. }
  321. function clear(section) {
  322. const outputEl = section.querySelector('.output')
  323. outputEl.classList.remove('error')
  324. outputEl.classList.remove('success')
  325.  
  326. const messageEl = window.messageEl
  327. messageEl.innerText = ''
  328. messageEl.classList.remove('info')
  329. messageEl.classList.remove('error')
  330. messageEl.classList.remove('success')
  331. }
  332. function success(msg, section) {
  333. const outputEl = section.querySelector('.output')
  334. outputEl.classList.add('success')
  335.  
  336. const messageEl = window.messageEl
  337. messageEl.innerText += msg + '\n'
  338. messageEl.classList.add('success')
  339. }
  340. function info(msg, section) {
  341. console.log(msg)
  342. const messageEl = window.messageEl
  343. messageEl.innerText += msg + '\n'
  344. messageEl.classList.add('info')
  345. }
  346. function error(msg, section) {
  347. const outputEl = section.querySelector('.output')
  348. outputEl.classList.add('error')
  349.  
  350. const messageEl = window.messageEl
  351. messageEl.innerText += msg + '\n'
  352. messageEl.classList.add('error')
  353. }
  354. function serialize(result) {
  355. return JSON.stringify(result, (k, v) => {
  356. if (Number.isNaN(v) || v === Infinity || v === -Infinity || v === undefined) return '' + v
  357. return v
  358. })
  359. }
  360. function parseArg(arg) {
  361. return JSON.parse(arg.trim())
  362. }
  363. function createButton(text, onClick) {
  364. const btn = document.createElement('button')
  365. btn.innerText = text
  366. btn.addEventListener('click', onClick)
  367. return btn
  368. }
  369. function createData(labelText, str = '') {
  370. const div = document.createElement('div');
  371.  
  372. const p = document.createElement('p')
  373. p.classList.add('title')
  374. const label = document.createElement('label')
  375. label.innerText = labelText
  376. p.appendChild(label);
  377. div.appendChild(p);
  378.  
  379. const textarea = document.createElement('textarea')
  380. textarea.setAttribute('class', labelText.toLowerCase())
  381. textarea.value = str;
  382. textarea.addEventListener('blur', () => saveCases())
  383. div.appendChild(textarea)
  384. return div
  385. }
  386. function parse(text) {
  387. const r = /(?:输入|Input)[::]([\s\S]*?)(?:输出|Output)[::][\s\n]*(?:.*:)?(.*)(\n|$)/ig
  388. const ans = []
  389. let match
  390. while(match = r.exec(text)) {
  391. const [, input, expected] = match
  392. ans.push({
  393. input: parseInput(input.trim()),
  394. expected: parseExpected(expected)
  395. })
  396. }
  397. return ans
  398. }
  399. function parseExpected(expected) {
  400. try {
  401. return JSON.stringify(JSON.parse(expected))
  402. } catch (err) {
  403. return expected
  404. }
  405. }
  406. function parseInput(input) {
  407. const args = []
  408. const pair = {
  409. "'": "'",
  410. '"': '"',
  411. '[': ']',
  412. '{': '}',
  413. '(': ')'
  414. }
  415. let state = 'input'
  416. let stack
  417. let arg
  418. for(let i = 0; i < input.length; i++) {
  419. const char = input.charAt(i)
  420. if (state === 'input') {
  421. if (char === '=') {
  422. state = 'expr'
  423. arg = ''
  424. stack = []
  425. }
  426. } else if (state === 'expr') {
  427. if ('"\'[]{}()'.includes(char) && input[i - 1] !== '\\') {
  428. if (pair[stack[stack.length - 1]] === char) stack.pop()
  429. else stack.push(char)
  430. arg += char
  431. } else if (stack.length) {
  432. arg += char
  433. } else if ((char === ',' || char === '\n') && stack.length === 0) {
  434. state = 'input'
  435. args.push(arg)
  436. arg = ''
  437. } else {
  438. arg += char
  439. }
  440. }
  441. }
  442. if (arg === undefined) args.push(input)
  443. else if (arg) args.push(arg)
  444. return args.map(x => x.split('\n').map(l => l.trim()).join(' ').trim()).join('\n')
  445. }
  446. })();