// ==UserScript==
// @name LeetCode Helper for JavaScript
// @namespace http://tampermonkey.net/
// @version 0.10
// @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.cn/problems/*
// @match https://leetcode.cn/contest/*/problems/*
// @match https://leetcode.com/problems/*
// @match https://leetcode.com/contest/*/problems/*
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/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')
}
})();