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.2
  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. // observe document.body for the language selection dropdown to be added
  173. const mutationObserver = new MutationObserver((mutations) => {
  174. for (const m of mutations) {
  175. if (m.type !== 'childList' || !m.addedNodes?.item(0)?.innerText?.startsWith('C++\nJava\nPython\nPython3')) {
  176. continue;
  177. }
  178. const switchLanguageBtn = document.querySelector('#editor > div:nth-child(1) button:nth-child(1)');
  179. const languageSelectionDiv = m.addedNodes[0].querySelector('div > div > div');
  180. const lastColDiv = languageSelectionDiv.lastElementChild;
  181. const moonDiv = lastColDiv.lastElementChild.cloneNode(true);
  182. moonDiv.querySelector('div > div > div').innerText = 'MoonBit';
  183. lastColDiv.appendChild(moonDiv);
  184. for (const colDiv of languageSelectionDiv.children) {
  185. for (const itemDiv of colDiv.children) {
  186. const svg = moonDiv.querySelector('div > div > svg');
  187. if (toLanguageId(itemDiv.innerText) === getCurrentLanguageId()) {
  188. svg.classList.add('visible');
  189. svg.classList.remove('invisible');
  190. } else {
  191. svg.classList.add('invisible');
  192. svg.classList.remove('visible');
  193. }
  194. itemDiv.onclick = async () => {
  195. await switchLanguage(itemDiv.innerText);
  196. switchLanguageBtn.firstChild.data = itemDiv.innerText;
  197. languageSelectionDiv.parentElement.hidden = true;
  198. };
  199. }
  200. }
  201. break;
  202. }
  203. });
  204. mutationObserver.observe(document.body, { childList: true });
  205. // observe for the "Reset to default code" dialog to be added
  206. waitUntil(() => {
  207. const topRightBtnsDiv = document.querySelector('#editor > div:nth-child(1) > div:last-child');
  208. if (topRightBtnsDiv === null) {
  209. return false;
  210. }
  211. const mutationObserver = new MutationObserver((mutations) => {
  212. if (getCurrentLanguageId() !== 'moonbit') {
  213. return;
  214. }
  215. for (const m of mutations) {
  216. const p = /您将放弃所有更改并初始化代码!|Your current code will be discarded and reset to the default code!/;
  217. if (m.type !== 'childList' || !m.addedNodes?.item(0)?.innerText?.match(p)) {
  218. continue;
  219. }
  220. const confirmBtn = m.addedNodes[0].querySelectorAll('button')[1];
  221. confirmBtn.onclick = async () => {
  222. globalThis.monaco.editor.getEditors()[0].getModel().setValue(await generateMoonCodeTemplate());
  223. };
  224. break;
  225. }
  226. });
  227. mutationObserver.observe(topRightBtnsDiv, { childList: true });
  228. return true;
  229. });
  230. // compile
  231. async function compile(commentSource = false) {
  232. const editor = globalThis.monaco.editor.getEditors()[0];
  233. const { name } = await getQuestionMetaData();
  234. const result = await globalThis.moon.compile({
  235. libInputs: [[`${name}.mbt`, editor.getValue()]],
  236. isMain: false,
  237. exportedFunctions: [name],
  238. });
  239. if (result.kind === 'success') {
  240. return `${commentSource && editor.getValue().trim().replace(/^/gm, '// ') || ''}\n${
  241. new TextDecoder().decode(result.js)
  242. .replace(/export\s*\{\s*([^\s]+)\s+as\s+([^\s]+)\s*\}/g, 'const $2 = $1;')}`;
  243. } else if (result.kind === 'error') {
  244. throw new Error(result.diagnostics.map((d) => `${name}.mbt:${d.loc.start.line} ${d.message}\n ${
  245. editor.getModel().getValueInRange({
  246. startLineNumber: d.loc.start.line,
  247. startColumn: d.loc.start.col,
  248. endLineeNumber: d.loc.end.line,
  249. endColumn: d.loc.end.col,
  250. })}`).join('\n\n'));
  251. }
  252. return null;
  253. }
  254. // run and submit
  255. globalThis._fetch = globalThis.fetch;
  256. globalThis.fetch = async (resource, options) => {
  257. // pre hook
  258. if ((resource === `${document.location.pathname}interpret_solution/` ||
  259. resource === `${document.location.pathname}submit/`) && getCurrentLanguageId() === 'moonbit') {
  260. const body = JSON.parse(options.body);
  261. body.lang = 'javascript';
  262. body.typed_code = await compile(true)
  263. .catch((e) => `throw'MOON_ERR_BEGIN\\n'+${JSON.stringify(e.message)}+'\\nMOON_ERR_END'`);
  264. options.body = JSON.stringify(body);
  265. }
  266. const r = globalThis._fetch(resource, options);
  267. // post hook
  268. if (resource.match(/^\/submissions\/detail\/[^/]+\/check\/$/)) {
  269. const checkResult = await r.then((resp) => resp.clone().json());
  270. const { full_runtime_error: fullRuntimeError = '' } = checkResult;
  271. const [_, moonError = null] = fullRuntimeError.match(/MOON_ERR_BEGIN\n([\s\S]+)\nMOON_ERR_END/) ?? [];
  272. if (_ && moonError) {
  273. checkResult.full_runtime_error = moonError;
  274. return Response.json(checkResult);
  275. }
  276. return r;
  277. }
  278. return r;
  279. };
  280. })();