LeetCode Helper for JavaScript

try to take over the world!

2022-05-14 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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')
    }
})();