Greasy Fork is available in English.

MoonBit ❤️ LeetCode

add support of moonbit language to leetcode

  1. // ==UserScript==
  2. // @name MoonBit ❤️ LeetCode
  3. // @namespace a23187.cn
  4. // @version 1.1.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. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (async function() {
  16. 'use strict';
  17. async function waitUntil(cond) {
  18. await new Promise((resolve) => {
  19. const id = setInterval(() => {
  20. if (cond()) {
  21. clearInterval(id);
  22. resolve();
  23. }
  24. }, 1000);
  25. });
  26. }
  27. // wait until the `globalThis.monaco` is presented
  28. await waitUntil(() => globalThis.monaco !== undefined);
  29. // init `moonpad`, `moon`
  30. let moonpad;
  31. let lspWorker;
  32. let mooncWorkerUrl;
  33. let onigWasmUrl;
  34. if (document.getElementById('moonbit-leetcode-crx-loader')) {
  35. const fetchCrxResource = (path) => new Promise((resolve) => {
  36. const f = (e) => {
  37. if (e.data && e.data.type === 'FETCH_CRX_RESOURCE_RESPONSE' && e.data.blob) {
  38. window.removeEventListener('message', f);
  39. resolve(e.data.blob);
  40. }
  41. };
  42. window.addEventListener('message', f);
  43. window.postMessage({ type: 'FETCH_CRX_RESOURCE', path }, '*');
  44. });
  45. const createObjectUrlFromCrxResource = async (path) => URL.createObjectURL(await fetchCrxResource(path));
  46. const createWorkerFromCrxResource = async (path) => new Worker(await createObjectUrlFromCrxResource(path));
  47. const basePath = 'moonpad-monaco';
  48. moonpad = await import(await createObjectUrlFromCrxResource(`${basePath}/moonpad-monaco.js`));
  49. lspWorker = await createWorkerFromCrxResource(`${basePath}/lsp-server.js`);
  50. mooncWorkerUrl = await createObjectUrlFromCrxResource(`${basePath}/moonc-worker.js`);
  51. onigWasmUrl = await createObjectUrlFromCrxResource(`${basePath}/onig.wasm`);
  52. } else {
  53. const createObjectUrlFromCORSUrl = async (url) => await fetch(url).then((resp) => resp.text())
  54. .then((cnt) => URL.createObjectURL(new Blob([cnt], { type: 'application/javascript' })));
  55. const createWorkerFromCORSUrl = async (url) => new Worker(await createObjectUrlFromCORSUrl(url));
  56. const baseUrl = 'https://cdn.jsdelivr.net/gh/A-23187/moonbit-leetcode/moonpad-monaco';
  57. moonpad = await import(`${baseUrl}/moonpad-monaco.js`);
  58. lspWorker = await createWorkerFromCORSUrl(`${baseUrl}/lsp-server.js`);
  59. mooncWorkerUrl = await createObjectUrlFromCORSUrl(`${baseUrl}/moonc-worker.js`);
  60. onigWasmUrl = `${baseUrl}/onig.wasm`;
  61. }
  62. function initMoon() {
  63. globalThis.moon = globalThis.moon ?? moonpad.init({
  64. onigWasmUrl,
  65. lspWorker,
  66. mooncWorkerFactory: () => new Worker(mooncWorkerUrl),
  67. codeLensFilter: () => false,
  68. });
  69. }
  70. globalThis.moon = null;
  71. globalThis.moonpad = moonpad;
  72. // handle language switching
  73. const toLanguageId = (function() {
  74. const languageMap = new Map([['C++', 'cpp'], ['C#', 'csharp'], ['Go', 'golang']]);
  75. return (languageName) => (
  76. languageMap.has(languageName) ? languageMap.get(languageName) : languageName.toLowerCase());
  77. })();
  78. function getCurrentLanguageId() {
  79. return globalThis.monaco.editor.getEditors()[0].getModel().getLanguageId();
  80. }
  81. async function getUser() {
  82. return (await fetch('https://leetcode.cn/graphql/', {
  83. method: 'POST',
  84. body: JSON.stringify({
  85. operationName: 'globalData',
  86. query: `query globalData {
  87. userStatus {
  88. realName userSlug username
  89. }
  90. }`,
  91. variables: {},
  92. }),
  93. }).then((resp) => resp.json())).data.userStatus;
  94. }
  95. function getQuestionTitleSlug() {
  96. return document.location.pathname.split('/')[2];
  97. }
  98. async function getQuestion() {
  99. return (await fetch('https://leetcode.cn/graphql/', {
  100. method: 'POST',
  101. body: JSON.stringify({
  102. operationName: 'questionDetail',
  103. query: `query questionDetail($titleSlug: String!) {
  104. question(titleSlug: $titleSlug) {
  105. titleSlug questionId questionFrontendId metaData
  106. }
  107. }`,
  108. variables: {
  109. titleSlug: getQuestionTitleSlug(),
  110. },
  111. }),
  112. }).then((resp) => resp.json())).data.question;
  113. }
  114. async function getQuestionMetaData() {
  115. return JSON.parse((await getQuestion()).metaData);
  116. }
  117. const parseType = (function() {
  118. const typeMap = new Map([
  119. ['void', 'Unit'], ['boolean', 'Bool'], ['integer', 'Int'], ['long', 'Int64'],
  120. ['float', 'Float'], ['double', 'Double'], ['char', 'Char'], ['character', 'Char'], ['string', 'String'],
  121. ]);
  122. const dfs = (type, begin, end) => {
  123. if (begin >= end) {
  124. return '';
  125. }
  126. if (type.endsWith('[]', end)) {
  127. return `Array[${dfs(type, begin, end - 2)}]`;
  128. }
  129. if (type.startsWith('list<', begin)) {
  130. return `Array[${dfs(type, begin + 5, end - 1)}]`;
  131. }
  132. const t = type.substring(begin, end);
  133. return typeMap.get(t) ?? t;
  134. };
  135. return (type) => dfs(type, 0, type.length);
  136. })();
  137. async function generateMoonCodeTemplate() {
  138. const { name, params, return: { type: returnType } } = await getQuestionMetaData();
  139. return `pub fn ${name}(${params.map((p) => `${p.name}: ${parseType(p.type)}`).join(', ')}) -> ${parseType(returnType)} {\n}\n`;
  140. }
  141. const switchLanguage = (function() {
  142. const monaco = globalThis.monaco;
  143. let moonModel = null;
  144. let nonMoonModel = null;
  145. return async (languageName) => {
  146. const currLanguageId = getCurrentLanguageId();
  147. const languageId = toLanguageId(languageName);
  148. if (currLanguageId === languageId) {
  149. return;
  150. }
  151. if (languageId === 'moonbit') {
  152. initMoon();
  153. if (moonModel === null) {
  154. const questionTitleSlug = getQuestionTitleSlug();
  155. moonModel = monaco.editor.createModel(localStorage.getItem(questionTitleSlug) ??
  156. await generateMoonCodeTemplate(), languageId);
  157. moonModel.onDidChangeContent(() => localStorage.setItem(questionTitleSlug, moonModel.getValue()));
  158. }
  159. monaco.editor.getEditors()[0].setModel(moonModel);
  160. } else if (currLanguageId === 'moonbit') {
  161. if (nonMoonModel === null) {
  162. const userSlug = (await getUser()).userSlug;
  163. const questionId = (await getQuestion()).questionId;
  164. const ugcKey = `ugc_${userSlug}_${questionId}_${languageId}_code`;
  165. nonMoonModel = monaco.editor
  166. .createModel(JSON.parse(localStorage.getItem(ugcKey))?.code ?? '', languageId);
  167. }
  168. monaco.editor.getEditors()[0].setModel(nonMoonModel);
  169. }
  170. };
  171. })();
  172. const mutationObserver = new MutationObserver((mutations) => {
  173. for (const m of mutations) {
  174. if (m.type !== 'childList' || !m.addedNodes?.item(0)?.innerText?.startsWith('C++\nJava\nPython\nPython3')) {
  175. continue;
  176. }
  177. const switchLanguageBtn = document.querySelector('#editor > div:nth-child(1) button:nth-child(1) > button');
  178. const languageSelectionDiv = m.addedNodes[0].querySelector('div > div > div');
  179. const lastColDiv = languageSelectionDiv.lastElementChild;
  180. const moonDiv = lastColDiv.lastElementChild.cloneNode(true);
  181. moonDiv.querySelector('div > div > div').innerText = 'MoonBit';
  182. lastColDiv.appendChild(moonDiv);
  183. for (const colDiv of languageSelectionDiv.children) {
  184. for (const itemDiv of colDiv.children) {
  185. const svg = moonDiv.querySelector('div > div > svg');
  186. if (toLanguageId(itemDiv.innerText) === getCurrentLanguageId()) {
  187. svg.classList.add('visible');
  188. svg.classList.remove('invisible');
  189. } else {
  190. svg.classList.add('invisible');
  191. svg.classList.remove('visible');
  192. }
  193. itemDiv.onclick = async () => {
  194. await switchLanguage(itemDiv.innerText);
  195. switchLanguageBtn.firstChild.data = itemDiv.innerText;
  196. };
  197. }
  198. }
  199. break;
  200. }
  201. });
  202. mutationObserver.observe(document.body, { childList: true });
  203. // compile
  204. async function compile(commentSource = false) {
  205. const editor = globalThis.monaco.editor.getEditors()[0];
  206. const { name } = await getQuestionMetaData();
  207. const result = await globalThis.moon.compile({
  208. libInputs: [[`${name}.mbt`, editor.getValue()]],
  209. isMain: false,
  210. exportedFunctions: [name],
  211. });
  212. if (result.kind === 'success') {
  213. return `${commentSource && editor.getValue().trim().replace(/^/gm, '// ') || ''}\n${
  214. new TextDecoder().decode(result.js)
  215. .replace(/export\s*\{\s*([^\s]+)\s+as\s+([^\s]+)\s*\}/g, 'const $2 = $1;')}`;
  216. } else if (result.kind === 'error') {
  217. throw new Error(result.diagnostics.map((d) => `${name}.mbt:${d.loc.start.line} ${d.message}\n ${
  218. editor.getModel().getValueInRange({
  219. startLineNumber: d.loc.start.line,
  220. startColumn: d.loc.start.col,
  221. endLineeNumber: d.loc.end.line,
  222. endColumn: d.loc.end.col,
  223. })}`).join('\n\n'));
  224. }
  225. return null;
  226. }
  227. // run and submit
  228. globalThis._fetch = globalThis.fetch;
  229. globalThis.fetch = async (resource, options) => {
  230. // pre hook
  231. if ((resource === `${document.location.pathname}interpret_solution/` ||
  232. resource === `${document.location.pathname}submit/`) && getCurrentLanguageId() === 'moonbit') {
  233. const body = JSON.parse(options.body);
  234. body.lang = 'javascript';
  235. body.typed_code = await compile(true)
  236. .catch((e) => `throw'MOON_ERR_BEGIN\\n'+${JSON.stringify(e.message)}+'\\nMOON_ERR_END'`);
  237. options.body = JSON.stringify(body);
  238. }
  239. const r = globalThis._fetch(resource, options);
  240. // post hook
  241. if (resource.match(/^\/submissions\/detail\/[^/]+\/check\/$/)) {
  242. const checkResult = await r.then((resp) => resp.clone().json());
  243. const { full_runtime_error: fullRuntimeError = '' } = checkResult;
  244. const [_, moonError = null] = fullRuntimeError.match(/MOON_ERR_BEGIN\n([\s\S]+)\nMOON_ERR_END/) ?? [];
  245. if (_ && moonError) {
  246. checkResult.full_runtime_error = moonError;
  247. return Response.json(checkResult);
  248. }
  249. return r;
  250. }
  251. return r;
  252. };
  253. })();