MoonBit ❤️ LeetCode

add support of moonbit language to leetcode

As of 2025-04-22. See the latest version.

  1. // ==UserScript==
  2. // @name MoonBit ❤️ LeetCode
  3. // @namespace a23187.cn
  4. // @version 1.0.0
  5. // @description add support of moonbit language to leetcode
  6. // @author A23187
  7. // @homepage https://github.com/A-23187/moonbit-leetcode
  8. // @match https://leetcode.cn/problems/*
  9. // @match https://leetcode.com/problems/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=leetcode.cn
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (async function() {
  15. 'use strict';
  16. async function waitUntil(cond) {
  17. await new Promise((resolve) => {
  18. const id = setInterval(() => {
  19. if (cond()) {
  20. clearInterval(id);
  21. resolve();
  22. }
  23. }, 1000);
  24. });
  25. }
  26. async function createObjectUrlFromCORSUrl(url) {
  27. return await fetch(url)
  28. .then((resp) => resp.text())
  29. .then((cnt) => URL.createObjectURL(new Blob([cnt], { type: 'application/javascript' })));
  30. }
  31. async function createWorkerFromCORSUrl(url) {
  32. return new Worker(await createObjectUrlFromCORSUrl(url));
  33. }
  34. // wait until the `globalThis.monaco` is presented
  35. await waitUntil(() => globalThis.monaco !== undefined);
  36. // init `moonpad`, `moon`
  37. const baseUrl = 'https://cdn.jsdelivr.net/gh/A-23187/moonbit-leetcode/moonpad-monaco';
  38. const moonpad = await import(`${baseUrl}/moonpad-monaco.js`);
  39. const lspWorker = await createWorkerFromCORSUrl(`${baseUrl}/lsp-server.js`);
  40. const mooncWorkerUrl = await createObjectUrlFromCORSUrl(`${baseUrl}/moonc-worker.js`);
  41. function initMoon() {
  42. globalThis.moon = globalThis.moon ?? moonpad.init({
  43. onigWasmUrl: `${baseUrl}/onig.wasm`,
  44. lspWorker,
  45. mooncWorkerFactory: () => new Worker(mooncWorkerUrl),
  46. codeLensFilter: () => false,
  47. });
  48. }
  49. globalThis.moon = null;
  50. globalThis.moonpad = moonpad;
  51. // handle language switching
  52. const toLanguageId = (function() {
  53. const languageMap = new Map([['C++', 'cpp'], ['C#', 'csharp'], ['Go', 'golang']]);
  54. return (languageName) => (
  55. languageMap.has(languageName) ? languageMap.get(languageName) : languageName.toLowerCase());
  56. })();
  57. function getCurrentLanguageId() {
  58. return globalThis.monaco.editor.getEditors()[0].getModel().getLanguageId();
  59. }
  60. function getQuestion() {
  61. const nextData = JSON.parse(document.getElementById('__NEXT_DATA__').innerText);
  62. const queries = nextData.props.pageProps.dehydratedState.queries;
  63. for (const q of queries) {
  64. for (const k of q.queryKey) {
  65. if (k === 'questionDetail') {
  66. return q.state.data.question;
  67. }
  68. }
  69. }
  70. return null;
  71. }
  72. function getQuestionMetaData() {
  73. return JSON.parse(getQuestion().metaData);
  74. }
  75. const parseType = (function() {
  76. const typeMap = new Map([
  77. ['void', 'Unit'], ['boolean', 'Bool'], ['integer', 'Int'], ['long', 'Int64'],
  78. ['float', 'Float'], ['double', 'Double'], ['char', 'Char'], ['string', 'String'],
  79. ]);
  80. const dfs = (type, begin, end) => {
  81. if (begin >= end) {
  82. return '';
  83. }
  84. if (type.endsWith('[]', end)) {
  85. return `Array[${dfs(type, begin, end - 2)}]`;
  86. }
  87. if (type.startsWith('list<', begin)) {
  88. return `Array[${dfs(type, begin + 5, end - 1)}]`;
  89. }
  90. const t = type.substring(begin, end);
  91. return typeMap.get(t) ?? t;
  92. };
  93. return (type) => dfs(type, 0, type.length);
  94. })();
  95. function generateMoonCodeTemplate() {
  96. const { name, params, return: { type: returnType } } = getQuestionMetaData();
  97. return `pub fn ${name}(${params.map((p) => `${p.name}: ${parseType(p.type)}`).join(', ')}) -> ${parseType(returnType)} {\n}\n`;
  98. }
  99. const switchLanguage = (function() {
  100. const monaco = globalThis.monaco;
  101. let moonModel = null;
  102. let nonMoonModel = null;
  103. return (languageName) => {
  104. const currLanguageId = getCurrentLanguageId();
  105. const languageId = toLanguageId(languageName);
  106. if (currLanguageId === languageId) {
  107. return;
  108. }
  109. if (languageId === 'moonbit') {
  110. initMoon();
  111. moonModel = moonModel ?? monaco.editor.createModel(generateMoonCodeTemplate(), languageId);
  112. monaco.editor.getEditors()[0].setModel(moonModel);
  113. } else if (currLanguageId === 'moonbit') {
  114. nonMoonModel = nonMoonModel ?? monaco.editor.createModel('', languageId);
  115. monaco.editor.getEditors()[0].setModel(nonMoonModel);
  116. }
  117. };
  118. })();
  119. const mutationObserver = new MutationObserver((mutations) => {
  120. for (const m of mutations) {
  121. if (m.type !== 'childList' || !m.addedNodes?.item(0)?.innerText?.startsWith('C++\nJava\nPython\nPython3')) {
  122. continue;
  123. }
  124. const switchLanguageBtn = document.querySelector('#editor > div:nth-child(1) button:nth-child(1) > button');
  125. const languageSelectionDiv = m.addedNodes[0].querySelector('div > div > div');
  126. const lastColDiv = languageSelectionDiv.lastElementChild;
  127. const moonDiv = lastColDiv.lastElementChild.cloneNode(true);
  128. moonDiv.querySelector('div > div > div').innerText = 'MoonBit';
  129. lastColDiv.appendChild(moonDiv);
  130. for (const colDiv of languageSelectionDiv.children) {
  131. for (const itemDiv of colDiv.children) {
  132. const svg = moonDiv.querySelector('div > div > svg');
  133. if (toLanguageId(itemDiv.innerText) === getCurrentLanguageId()) {
  134. svg.classList.add('visible');
  135. svg.classList.remove('invisible');
  136. } else {
  137. svg.classList.add('invisible');
  138. svg.classList.remove('visible');
  139. }
  140. itemDiv.onclick = () => {
  141. switchLanguage(itemDiv.innerText);
  142. switchLanguageBtn.firstChild.data = itemDiv.innerText;
  143. };
  144. }
  145. }
  146. break;
  147. }
  148. });
  149. mutationObserver.observe(document.body, { childList: true });
  150. // compile
  151. async function compile() {
  152. const editor = globalThis.monaco.editor.getEditors()[0];
  153. const { name } = getQuestionMetaData();
  154. const result = await globalThis.moon.compile({
  155. libInputs: [[`${name}.mbt`, editor.getValue()]],
  156. isMain: false,
  157. exportedFunctions: [name],
  158. });
  159. if (result.kind === 'success') {
  160. return new TextDecoder().decode(result.js)
  161. .replace(/export\s*\{\s*([^\s]+)\s+as\s+([^\s]+)\s*\}/g, 'const $2 = $1;');
  162. } else if (result.kind === 'error') {
  163. throw new Error(result.diagnostics.map((d) => `${name}.mbt:${d.loc.start.line} ${d.message}\n ${
  164. editor.getModel().getValueInRange({
  165. startLineNumber: d.loc.start.line,
  166. startColumn: d.loc.start.col,
  167. endLineeNumber: d.loc.end.line,
  168. endColumn: d.loc.end.col,
  169. })}`).join('\n\n'));
  170. }
  171. return null;
  172. }
  173. // run and submit
  174. globalThis._fetch = globalThis.fetch;
  175. globalThis.fetch = async (resource, options) => {
  176. // pre hook
  177. if ((resource === `${document.location.pathname}interpret_solution/` ||
  178. resource === `${document.location.pathname}submit/`) && getCurrentLanguageId() === 'moonbit') {
  179. const body = JSON.parse(options.body);
  180. body.lang = 'javascript';
  181. body.typed_code = await compile()
  182. .catch((e) => `throw'MOON_ERR_BEGIN\\n'+${JSON.stringify(e.message)}+'\\nMOON_ERR_END'`);
  183. options.body = JSON.stringify(body);
  184. }
  185. const r = globalThis._fetch(resource, options);
  186. // post hook
  187. if (resource.match(/^\/submissions\/detail\/[^/]+\/check\/$/)) {
  188. const checkResult = await r.then((resp) => resp.clone().json());
  189. const { full_runtime_error: fullRuntimeError = '' } = checkResult;
  190. const [_, moonError = null] = fullRuntimeError.match(/MOON_ERR_BEGIN\n([\s\S]+)\nMOON_ERR_END/) ?? [];
  191. if (_ && moonError) {
  192. checkResult.full_runtime_error = moonError;
  193. return Response.json(checkResult);
  194. }
  195. return r;
  196. }
  197. return r;
  198. };
  199. })();