- // ==UserScript==
- // @name LeetCode Helper for JavaScript
- // @namespace http://tampermonkey.net/
- // @version 0.9
- // @description try to take over the world!
- // @author You
- // @match https://leetcode-cn.com/problems/*
- // @match https://leetcode-cn.com/contest/*/problems/*
- // @match https://leetcode.com/problems/*
- // @match https://leetcode.com/contest/*/problems/*
- // @grant GM_setValue
- // @grant GM_getValue
- // ==/UserScript==
-
- (function() {
- console.log('loading leetcode helper');
- const STYLE = `
- .leetcode-helper {
- position: fixed;
- background: rgba(255,255,255,1);
- z-index: 1024;
- width: 400px;
- min-height: 50px;
- top: 0;
- left: 0;
- border: .5px solid rgb(255, 109, 0);
- max-height: 500px;
- overflow-y: auto;
- }
- .leetcode-helper-header {
- font-weight: bold;
- color: white;
- background: rgb(255, 164, 97);
- padding: 2px 7px;
- position: sticky;
- top: 0;
- }
- .leetcode-helper-body {
- padding: 5px 10px;
- }
- .leetcode-helper-body .case {
- display: flex;
- justify-content: space-between;
- }
- .leetcode-helper-body .case>div {
- flex: 0 1 30%;
- overflow: auto;
- }
- .leetcode-helper-body section>div:last-child {
- flex: 0 1 60%;
- }
- .leetcode-helper-body section p {
- margin-bottom: 0px;
- }
- .leetcode-helper-body label {
- color: rgb(255, 109, 0); margin-right: 5px;'
- }
- .leetcode-helper-status {
- margin-left: 5px;
- }
- .leetcode-helper .case .title button {
- line-height: 12px;
- font-size: 12px;
- }
- .leetcode-helper .case textarea {
- width: 100%;
- overflow: auto;
- white-space: nowrap;
- border: 1px solid black;
- border-left: none;
- font-family: monospace;
- }
- .leetcode-helper .case div:first-child textarea {
- border-left: 1px solid black;
- }
- .leetcode-helper .success {
- background-color: lightgreen;
- }
- .leetcode-helper .error {
- background-color: #ff9090;
- }
- .leetcode-helper .message {
- white-space: pre;
- font-family: monospace;
- line-height: 1.2;
- padding: 2px 5px;
- max-height: 20em;
- overflow: auto;
- }
- .leetcode-helper .operations {
- margin-top: 5px;
- }
- `
-
- main();
-
- async function main() {
- insertStyleSheets();
- const panel = createPanel()
- console.log('panel created:', panel);
- setDraggable(panel.querySelector('.leetcode-helper-header'), panel);
- document.body.appendChild(panel);
- }
-
- function getEditorText() {
- if (typeof monaco !== 'undefined') { // window is not the window, so window.monaco wont't work
- return monaco.editor.getModels()[0].getValue()
- }
- const el1 = document.querySelector('.editor-scrollable')
- if (el1) return el1.innerText
-
- const el2 = document.querySelector('.CodeMirror')
- if (el2) return el2.CodeMirror.getValue()
-
- return 'editor not found'
- }
- function getResolver(log) {
- const body = getEditorText()
- const match = /var\s+([a-zA-Z_$][\w$]*)\s*=/.exec(body)
- if (!match) throw new Error('resolver var xxx = function(){} not found')
- const fn = new Function(`console`, `${body}\n return ${match[1]}`)
- return fn({
- log: function(...args) {
- log(args.map(serialize).join(' '))
- },
- error: function(...args) {
- log(args.map(serialize).join(' '))
- }
- })
- }
- function lineOffset() {
- try {
- const fn = new Function('console', 'throw new Error(314)')
- fn()
- } catch(e){
- const match = /(\d+):\d+\)($|\n)/.exec(e.stack)
- return match ? +match[1] - 1 : 2
- }
- }
-
- function insertStyleSheets() {
- const style = document.createElement('style')
- style.innerHTML = STYLE
- document.body.appendChild(style)
- }
-
- function getDescription() {
- return new Promise((resolve, reject) => {
- const interval = setInterval(() => {
- const el1 = document.querySelector('[data-key=description-content]')
- const el2 = document.querySelector('.question-content')
- const content = el1 && el1.innerText || el2 && el2.innerText
- if (!content) return
- clearInterval(interval)
- resolve(content)
- }, 300);
- })
- }
-
- function setDraggable(handle, panel) {
- let dragging = false
- let initX
- let initY
- let initMarginX
- let initMarginY
- handle.addEventListener('mousedown', e => {
- dragging = true
- initX = e.clientX - (panel.style.left.slice(0, -2) || 0)
- initY = e.clientY - (panel.style.top.slice(0, -2) || 0)
- // console.log(mousedown, recording (${initX}, ${initY}))
- })
- window.addEventListener('mousemove', e => {
- if (!dragging) return
- const l = Math.min(window.innerWidth - 15, Math.max(0, e.clientX - initX))
- const t = Math.min(window.innerHeight - 15, Math.max(0, e.clientY - initY))
- // console.log(moving to (${l}, ${r}));
- panel.style.left = l + 'px'
- panel.style.top = t + 'px'
- })
- window.addEventListener('mouseup', e => {
- dragging = false
- GM_setValue('pos', [+panel.style.left.slice(0, -2), +panel.style.top.slice(0, -2)])
- })
- }
- function renderCases (ios, caseList) {
- for(const io of ios) {
- caseList.append(createCase(io.input, io.expected))
- }
- }
- function loadCases () {
- let ios
- try {
- ios = JSON.parse(GM_getValue('leetcode.io:' + location.href))
- } catch (err) {
- return false
- }
- if (!ios) return false
- return ios
- }
- async function saveCases () {
- const sections = document.querySelectorAll('.leetcode-helper .case-list .case')
- const ios = [...sections].map(section => ({
- input: section.querySelector('.input').value,
- expected: section.querySelector('.expected').value
- }))
- GM_setValue('leetcode.io:' + location.href, JSON.stringify(ios))
- console.log('cases saved', ios)
- }
- async function parseIO(caseList) {
- console.log('parsing IO from HTML...')
- const desc = await getDescription();
- const ios = parse(desc);
- console.log('parsed sample input/expected', ios);
- renderCases(ios, caseList);
- if (ios.length === 0) info('sample input/output not found')
- else saveCases(ios)
- }
- function createPanel() {
- const panel = document.createElement('div');
- panel.setAttribute('class', 'leetcode-helper');
- const pos = GM_getValue('pos')
- if (pos) {
- panel.style.left = Math.min(pos[0], window.innerWidth - 50) + 'px'
- panel.style.top = Math.min(pos[1], window.innerHeight - 50) + 'px'
- }
-
- const header = document.createElement('div');
- header.innerText = 'LeetCode Helper';
- header.setAttribute('class', 'leetcode-helper-header');
- panel.appendChild(header);
-
- const body = document.createElement('div');
- body.setAttribute('class', 'leetcode-helper-body');
- panel.appendChild(body);
-
- const caseList = document.createElement('div')
- caseList.classList.add('case-list')
- body.appendChild(caseList)
-
- window.messageEl = document.createElement('div')
- window.messageEl.classList.add('message')
- body.appendChild(window.messageEl);
-
- const operations = document.createElement('div')
- operations.classList.add('operations')
- operations.appendChild(createButton('RunAll', x => runAll(caseList.querySelectorAll('.case'))))
- operations.appendChild(createButton('AddCase', () => caseList.append(createCase())))
- operations.appendChild(createButton('Refresh', () => {
- caseList.innerHTML = ''
- parseIO(caseList)
- }))
- body.appendChild(operations)
-
- const ios = loadCases()
- if (ios) renderCases(ios, caseList);
- else parseIO(caseList);
- return panel;
- }
- function createCase(input = '', expected = '') {
- const section = document.createElement('section')
- section.classList.add('case')
- section.appendChild(createData('Input', input))
- section.appendChild(createData('Expected', expected))
-
- const output = createData('Output', '')
- output.querySelector('.title').appendChild(createButton('Run', () => run(section)))
- output.querySelector('.title').appendChild(createButton('Delete', () => section.remove()))
- section.appendChild(output)
- return section
- }
- function run(section) {
- const input = section.querySelector('.input').value
- const expected = section.querySelector('.expected').value
- const outputEl = section.querySelector('.output')
- info('Running...', section)
-
- requestAnimationFrame(() => requestAnimationFrame(() => {
- let args
- try {
- args = input.split('\n').map(parseArg)
- } catch (err) {
- outputEl.value = err.stack.split('\n').map(x => x.replace(/\([^:]*:[^:]*:/, '(')).join('\n')
- console.error(err)
- return error(outputEl.value, section)
- }
- console.log('calling resolver with', ...args)
-
- clear(section)
- let result = null
- let resolver
- try {
- fn = getResolver(x => info(x, section))
- } catch (err) {
- outputEl.value = err.stack.split('\n').slice(0, -3).join('\n')
- console.error(err)
- return error(outputEl.value, section)
- }
- try {
- result = fn(...args)
- console.log('result:', result)
- } catch(err) {
- const offset = lineOffset();
- const fixLineNumber = line => line.replace(/(\d+):(\d+)\)$/, (match, line, col) => `${line - offset}:${col})`)
- outputEl.value = err.stack.split('\n').slice(0, -2).map(x => x.replace(/eval at [^)]*\), <[^>]*>:/, '')).map(fixLineNumber).join('\n')
- console.error(err)
- return error(outputEl.value, section)
- }
- const output = serialize(result)
- outputEl.value = output
- if (output === expected) {
- success('Accepted', section)
- } else {
- error('Wrong Answer', section)
- console.error(`Failed:\nExpected: ${expected}\nOutput: ${output}`)
- }
- }))
- }
- function runAll(sections) {
- for(const section of sections) run(section)
- }
- function clear(section) {
- const outputEl = section.querySelector('.output')
- outputEl.classList.remove('error')
- outputEl.classList.remove('success')
-
- const messageEl = window.messageEl
- messageEl.innerText = ''
- messageEl.classList.remove('info')
- messageEl.classList.remove('error')
- messageEl.classList.remove('success')
- }
- function success(msg, section) {
- const outputEl = section.querySelector('.output')
- outputEl.classList.add('success')
-
- const messageEl = window.messageEl
- messageEl.innerText += msg + '\n'
- messageEl.classList.add('success')
- }
- function info(msg, section) {
- console.log(msg)
- const messageEl = window.messageEl
- messageEl.innerText += msg + '\n'
- messageEl.classList.add('info')
- }
- function error(msg, section) {
- const outputEl = section.querySelector('.output')
- outputEl.classList.add('error')
-
- const messageEl = window.messageEl
- messageEl.innerText += msg + '\n'
- messageEl.classList.add('error')
- }
- function serialize(result) {
- return JSON.stringify(result, (k, v) => {
- if (Number.isNaN(v) || v === Infinity || v === -Infinity || v === undefined || typeof v === 'bigint') return '' + v
- return v
- })
- }
- function parseArg(arg) {
- return JSON.parse(arg.trim())
- }
- function createButton(text, onClick) {
- const btn = document.createElement('button')
- btn.innerText = text
- btn.addEventListener('click', onClick)
- return btn
- }
- function createData(labelText, str = '') {
- const div = document.createElement('div');
-
- const p = document.createElement('p')
- p.classList.add('title')
- const label = document.createElement('label')
- label.innerText = labelText
- p.appendChild(label);
- div.appendChild(p);
-
- const textarea = document.createElement('textarea')
- textarea.setAttribute('class', labelText.toLowerCase())
- textarea.value = str;
- textarea.addEventListener('blur', () => saveCases())
- div.appendChild(textarea)
- return div
- }
- function parse(text) {
- const r = /(?:输入|Input)[::]([\s\S]*?)(?:输出|Output)[::][\s\n]*(?:.*:)?(.*)(\n|$)/ig
- const ans = []
- let match
- while(match = r.exec(text)) {
- const [, input, expected] = match
- ans.push({
- input: parseInput(input.trim()),
- expected: parseExpected(expected)
- })
- }
- return ans
- }
- function parseExpected(expected) {
- try {
- return JSON.stringify(JSON.parse(expected))
- } catch (err) {
- return expected
- }
- }
- function parseInput(input) {
- const args = []
- const pair = {
- "'": "'",
- '"': '"',
- '[': ']',
- '{': '}',
- '(': ')'
- }
- let state = 'input'
- let stack
- let arg
- for(let i = 0; i < input.length; i++) {
- const char = input.charAt(i)
- if (state === 'input') {
- if (char === '=') {
- state = 'expr'
- arg = ''
- stack = []
- }
- } else if (state === 'expr') {
- if ('"\'[]{}()'.includes(char) && input[i - 1] !== '\\') {
- if (pair[stack[stack.length - 1]] === char) stack.pop()
- else stack.push(char)
- arg += char
- } else if (stack.length) {
- arg += char
- } else if ((char === ',' || char === '\n') && stack.length === 0) {
- state = 'input'
- args.push(arg)
- arg = ''
- } else {
- arg += char
- }
- }
- }
- if (arg === undefined) args.push(input)
- else if (arg) args.push(arg)
- return args.map(x => x.split('\n').map(l => l.trim()).join(' ').trim()).join('\n')
- }
- })();