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