MoonBit ❤️ LeetCode

add support of moonbit language to leetcode

Mint 2025.04.22.. Lásd a legutóbbi verzió

// ==UserScript==
// @name         MoonBit ❤️ LeetCode
// @namespace    a23187.cn
// @version      1.0.0
// @description  add support of moonbit language to leetcode
// @author       A23187
// @homepage     https://github.com/A-23187/moonbit-leetcode
// @match        https://leetcode.cn/problems/*
// @match        https://leetcode.com/problems/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=leetcode.cn
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';
    async function waitUntil(cond) {
        await new Promise((resolve) => {
            const id = setInterval(() => {
                if (cond()) {
                    clearInterval(id);
                    resolve();
                }
            }, 1000);
        });
    }
    async function createObjectUrlFromCORSUrl(url) {
        return await fetch(url)
            .then((resp) => resp.text())
            .then((cnt) => URL.createObjectURL(new Blob([cnt], { type: 'application/javascript' })));
    }
    async function createWorkerFromCORSUrl(url) {
        return new Worker(await createObjectUrlFromCORSUrl(url));
    }
    // wait until the `globalThis.monaco` is presented
    await waitUntil(() => globalThis.monaco !== undefined);
    // init `moonpad`, `moon`
    const baseUrl = 'https://cdn.jsdelivr.net/gh/A-23187/moonbit-leetcode/moonpad-monaco';
    const moonpad = await import(`${baseUrl}/moonpad-monaco.js`);
    const lspWorker = await createWorkerFromCORSUrl(`${baseUrl}/lsp-server.js`);
    const mooncWorkerUrl = await createObjectUrlFromCORSUrl(`${baseUrl}/moonc-worker.js`);
    function initMoon() {
        globalThis.moon = globalThis.moon ?? moonpad.init({
            onigWasmUrl: `${baseUrl}/onig.wasm`,
            lspWorker,
            mooncWorkerFactory: () => new Worker(mooncWorkerUrl),
            codeLensFilter: () => false,
        });
    }
    globalThis.moon = null;
    globalThis.moonpad = moonpad;
    // handle language switching
    const toLanguageId = (function() {
        const languageMap = new Map([['C++', 'cpp'], ['C#', 'csharp'], ['Go', 'golang']]);
        return (languageName) => (
            languageMap.has(languageName) ? languageMap.get(languageName) : languageName.toLowerCase());
    })();
    function getCurrentLanguageId() {
        return globalThis.monaco.editor.getEditors()[0].getModel().getLanguageId();
    }
    function getQuestion() {
        const nextData = JSON.parse(document.getElementById('__NEXT_DATA__').innerText);
        const queries = nextData.props.pageProps.dehydratedState.queries;
        for (const q of queries) {
            for (const k of q.queryKey) {
                if (k === 'questionDetail') {
                    return q.state.data.question;
                }
            }
        }
        return null;
    }
    function getQuestionMetaData() {
        return JSON.parse(getQuestion().metaData);
    }
    const parseType = (function() {
        const typeMap = new Map([
            ['void', 'Unit'], ['boolean', 'Bool'], ['integer', 'Int'], ['long', 'Int64'],
            ['float', 'Float'], ['double', 'Double'], ['char', 'Char'], ['string', 'String'],
        ]);
        const dfs = (type, begin, end) => {
            if (begin >= end) {
                return '';
            }
            if (type.endsWith('[]', end)) {
                return `Array[${dfs(type, begin, end - 2)}]`;
            }
            if (type.startsWith('list<', begin)) {
                return `Array[${dfs(type, begin + 5, end - 1)}]`;
            }
            const t = type.substring(begin, end);
            return typeMap.get(t) ?? t;
        };
        return (type) => dfs(type, 0, type.length);
    })();
    function generateMoonCodeTemplate() {
        const { name, params, return: { type: returnType } } = getQuestionMetaData();
        return `pub fn ${name}(${params.map((p) => `${p.name}: ${parseType(p.type)}`).join(', ')}) -> ${parseType(returnType)} {\n}\n`;
    }
    const switchLanguage = (function() {
        const monaco = globalThis.monaco;
        let moonModel = null;
        let nonMoonModel = null;
        return (languageName) => {
            const currLanguageId = getCurrentLanguageId();
            const languageId = toLanguageId(languageName);
            if (currLanguageId === languageId) {
                return;
            }
            if (languageId === 'moonbit') {
                initMoon();
                moonModel = moonModel ?? monaco.editor.createModel(generateMoonCodeTemplate(), languageId);
                monaco.editor.getEditors()[0].setModel(moonModel);
            } else if (currLanguageId === 'moonbit') {
                nonMoonModel = nonMoonModel ?? monaco.editor.createModel('', languageId);
                monaco.editor.getEditors()[0].setModel(nonMoonModel);
            }
        };
    })();
    const mutationObserver = new MutationObserver((mutations) => {
        for (const m of mutations) {
            if (m.type !== 'childList' || !m.addedNodes?.item(0)?.innerText?.startsWith('C++\nJava\nPython\nPython3')) {
                continue;
            }
            const switchLanguageBtn = document.querySelector('#editor > div:nth-child(1) button:nth-child(1) > button');
            const languageSelectionDiv = m.addedNodes[0].querySelector('div > div > div');
            const lastColDiv = languageSelectionDiv.lastElementChild;
            const moonDiv = lastColDiv.lastElementChild.cloneNode(true);
            moonDiv.querySelector('div > div > div').innerText = 'MoonBit';
            lastColDiv.appendChild(moonDiv);
            for (const colDiv of languageSelectionDiv.children) {
                for (const itemDiv of colDiv.children) {
                    const svg = moonDiv.querySelector('div > div > svg');
                    if (toLanguageId(itemDiv.innerText) === getCurrentLanguageId()) {
                        svg.classList.add('visible');
                        svg.classList.remove('invisible');
                    } else {
                        svg.classList.add('invisible');
                        svg.classList.remove('visible');
                    }
                    itemDiv.onclick = () => {
                        switchLanguage(itemDiv.innerText);
                        switchLanguageBtn.firstChild.data = itemDiv.innerText;
                    };
                }
            }
            break;
        }
    });
    mutationObserver.observe(document.body, { childList: true });
    // compile
    async function compile() {
        const editor = globalThis.monaco.editor.getEditors()[0];
        const { name } = getQuestionMetaData();
        const result = await globalThis.moon.compile({
            libInputs: [[`${name}.mbt`, editor.getValue()]],
            isMain: false,
            exportedFunctions: [name],
        });
        if (result.kind === 'success') {
            return new TextDecoder().decode(result.js)
                .replace(/export\s*\{\s*([^\s]+)\s+as\s+([^\s]+)\s*\}/g, 'const $2 = $1;');
        } else if (result.kind === 'error') {
            throw new Error(result.diagnostics.map((d) => `${name}.mbt:${d.loc.start.line} ${d.message}\n    ${
                editor.getModel().getValueInRange({
                    startLineNumber: d.loc.start.line,
                    startColumn: d.loc.start.col,
                    endLineeNumber: d.loc.end.line,
                    endColumn: d.loc.end.col,
                })}`).join('\n\n'));
        }
        return null;
    }
    // run and submit
    globalThis._fetch = globalThis.fetch;
    globalThis.fetch = async (resource, options) => {
        // pre hook
        if ((resource === `${document.location.pathname}interpret_solution/` ||
             resource === `${document.location.pathname}submit/`) && getCurrentLanguageId() === 'moonbit') {
            const body = JSON.parse(options.body);
            body.lang = 'javascript';
            body.typed_code = await compile()
                .catch((e) => `throw'MOON_ERR_BEGIN\\n'+${JSON.stringify(e.message)}+'\\nMOON_ERR_END'`);
            options.body = JSON.stringify(body);
        }
        const r = globalThis._fetch(resource, options);
        // post hook
        if (resource.match(/^\/submissions\/detail\/[^/]+\/check\/$/)) {
            const checkResult = await r.then((resp) => resp.clone().json());
            const { full_runtime_error: fullRuntimeError = '' } = checkResult;
            const [_, moonError = null] = fullRuntimeError.match(/MOON_ERR_BEGIN\n([\s\S]+)\nMOON_ERR_END/) ?? [];
            if (_ && moonError) {
                checkResult.full_runtime_error = moonError;
                return Response.json(checkResult);
            }
            return r;
        }
        return r;
    };
})();