CodeBlock.svelte 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <script lang="ts">
  2. import hljs from 'highlight.js';
  3. import { loadPyodide } from 'pyodide';
  4. import mermaid from 'mermaid';
  5. import { v4 as uuidv4 } from 'uuid';
  6. import { getContext, getAllContexts, onMount, tick, createEventDispatcher } from 'svelte';
  7. import { copyToClipboard } from '$lib/utils';
  8. import 'highlight.js/styles/github-dark.min.css';
  9. import PyodideWorker from '$lib/workers/pyodide.worker?worker';
  10. import CodeEditor from '$lib/components/common/CodeEditor.svelte';
  11. const i18n = getContext('i18n');
  12. const dispatch = createEventDispatcher();
  13. export let id = '';
  14. export let save = false;
  15. export let token;
  16. export let lang = '';
  17. export let code = '';
  18. let _code = '';
  19. $: if (code) {
  20. updateCode();
  21. }
  22. const updateCode = () => {
  23. _code = code;
  24. };
  25. let _token = null;
  26. let mermaidHtml = null;
  27. let highlightedCode = null;
  28. let executing = false;
  29. let stdout = null;
  30. let stderr = null;
  31. let result = null;
  32. let copied = false;
  33. let saved = false;
  34. const saveCode = () => {
  35. saved = true;
  36. code = _code;
  37. dispatch('save', code);
  38. setTimeout(() => {
  39. saved = false;
  40. }, 1000);
  41. };
  42. const copyCode = async () => {
  43. copied = true;
  44. await copyToClipboard(code);
  45. setTimeout(() => {
  46. copied = false;
  47. }, 1000);
  48. };
  49. const checkPythonCode = (str) => {
  50. // Check if the string contains typical Python syntax characters
  51. const pythonSyntax = [
  52. 'def ',
  53. 'else:',
  54. 'elif ',
  55. 'try:',
  56. 'except:',
  57. 'finally:',
  58. 'yield ',
  59. 'lambda ',
  60. 'assert ',
  61. 'nonlocal ',
  62. 'del ',
  63. 'True',
  64. 'False',
  65. 'None',
  66. ' and ',
  67. ' or ',
  68. ' not ',
  69. ' in ',
  70. ' is ',
  71. ' with '
  72. ];
  73. for (let syntax of pythonSyntax) {
  74. if (str.includes(syntax)) {
  75. return true;
  76. }
  77. }
  78. // If none of the above conditions met, it's probably not Python code
  79. return false;
  80. };
  81. const executePython = async (code) => {
  82. if (!code.includes('input') && !code.includes('matplotlib')) {
  83. executePythonAsWorker(code);
  84. } else {
  85. result = null;
  86. stdout = null;
  87. stderr = null;
  88. executing = true;
  89. document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
  90. let pyodide = await loadPyodide({
  91. indexURL: '/pyodide/',
  92. stdout: (text) => {
  93. console.log('Python output:', text);
  94. if (stdout) {
  95. stdout += `${text}\n`;
  96. } else {
  97. stdout = `${text}\n`;
  98. }
  99. },
  100. stderr: (text) => {
  101. console.log('An error occured:', text);
  102. if (stderr) {
  103. stderr += `${text}\n`;
  104. } else {
  105. stderr = `${text}\n`;
  106. }
  107. },
  108. packages: ['micropip']
  109. });
  110. try {
  111. const micropip = pyodide.pyimport('micropip');
  112. // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
  113. let packages = [
  114. code.includes('requests') ? 'requests' : null,
  115. code.includes('bs4') ? 'beautifulsoup4' : null,
  116. code.includes('numpy') ? 'numpy' : null,
  117. code.includes('pandas') ? 'pandas' : null,
  118. code.includes('matplotlib') ? 'matplotlib' : null,
  119. code.includes('sklearn') ? 'scikit-learn' : null,
  120. code.includes('scipy') ? 'scipy' : null,
  121. code.includes('re') ? 'regex' : null,
  122. code.includes('seaborn') ? 'seaborn' : null
  123. ].filter(Boolean);
  124. console.log(packages);
  125. await micropip.install(packages);
  126. result = await pyodide.runPythonAsync(`from js import prompt
  127. def input(p):
  128. return prompt(p)
  129. __builtins__.input = input`);
  130. result = await pyodide.runPython(code);
  131. if (!result) {
  132. result = '[NO OUTPUT]';
  133. }
  134. console.log(result);
  135. console.log(stdout);
  136. console.log(stderr);
  137. const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
  138. if (pltCanvasElement?.innerHTML !== '') {
  139. pltCanvasElement.classList.add('pt-4');
  140. }
  141. } catch (error) {
  142. console.error('Error:', error);
  143. stderr = error;
  144. }
  145. executing = false;
  146. }
  147. };
  148. const executePythonAsWorker = async (code) => {
  149. result = null;
  150. stdout = null;
  151. stderr = null;
  152. executing = true;
  153. let packages = [
  154. code.includes('requests') ? 'requests' : null,
  155. code.includes('bs4') ? 'beautifulsoup4' : null,
  156. code.includes('numpy') ? 'numpy' : null,
  157. code.includes('pandas') ? 'pandas' : null,
  158. code.includes('sklearn') ? 'scikit-learn' : null,
  159. code.includes('scipy') ? 'scipy' : null,
  160. code.includes('re') ? 'regex' : null,
  161. code.includes('seaborn') ? 'seaborn' : null
  162. ].filter(Boolean);
  163. console.log(packages);
  164. const pyodideWorker = new PyodideWorker();
  165. pyodideWorker.postMessage({
  166. id: id,
  167. code: code,
  168. packages: packages
  169. });
  170. setTimeout(() => {
  171. if (executing) {
  172. executing = false;
  173. stderr = 'Execution Time Limit Exceeded';
  174. pyodideWorker.terminate();
  175. }
  176. }, 60000);
  177. pyodideWorker.onmessage = (event) => {
  178. console.log('pyodideWorker.onmessage', event);
  179. const { id, ...data } = event.data;
  180. console.log(id, data);
  181. data['stdout'] && (stdout = data['stdout']);
  182. data['stderr'] && (stderr = data['stderr']);
  183. data['result'] && (result = data['result']);
  184. executing = false;
  185. };
  186. pyodideWorker.onerror = (event) => {
  187. console.log('pyodideWorker.onerror', event);
  188. executing = false;
  189. };
  190. };
  191. let debounceTimeout;
  192. const drawMermaidDiagram = async () => {
  193. try {
  194. if (await mermaid.parse(code)) {
  195. const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
  196. mermaidHtml = svg;
  197. }
  198. } catch (error) {
  199. console.log('Error:', error);
  200. }
  201. };
  202. const render = async () => {
  203. if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
  204. (async () => {
  205. await drawMermaidDiagram();
  206. })();
  207. }
  208. };
  209. $: if (token) {
  210. if (JSON.stringify(token) !== JSON.stringify(_token)) {
  211. _token = token;
  212. }
  213. }
  214. $: if (_token) {
  215. render();
  216. }
  217. $: if (lang) {
  218. dispatch('code', { lang });
  219. }
  220. onMount(async () => {
  221. console.log('codeblock', lang, code);
  222. if (lang) {
  223. dispatch('code', { lang });
  224. }
  225. if (document.documentElement.classList.contains('dark')) {
  226. mermaid.initialize({
  227. startOnLoad: true,
  228. theme: 'dark',
  229. securityLevel: 'loose'
  230. });
  231. } else {
  232. mermaid.initialize({
  233. startOnLoad: true,
  234. theme: 'default',
  235. securityLevel: 'loose'
  236. });
  237. }
  238. });
  239. </script>
  240. <div>
  241. <div class="relative my-2 flex flex-col rounded-lg" dir="ltr">
  242. {#if lang === 'mermaid'}
  243. {#if mermaidHtml}
  244. {@html `${mermaidHtml}`}
  245. {:else}
  246. <pre class="mermaid">{code}</pre>
  247. {/if}
  248. {:else}
  249. <div class="text-text-300 absolute pl-4 py-1.5 text-xs font-medium dark:text-white">
  250. {lang}
  251. </div>
  252. <div
  253. class="sticky top-8 mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
  254. >
  255. <div class="flex items-center gap-0.5 translate-y-[1px]">
  256. {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
  257. {#if executing}
  258. <div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
  259. {:else}
  260. <button
  261. class="run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  262. on:click={async () => {
  263. code = _code;
  264. await tick();
  265. executePython(code);
  266. }}>{$i18n.t('Run')}</button
  267. >
  268. {/if}
  269. {/if}
  270. {#if save}
  271. <button
  272. class="save-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  273. on:click={saveCode}
  274. >
  275. {saved ? $i18n.t('Saved') : $i18n.t('Save')}
  276. </button>
  277. {/if}
  278. <button
  279. class="copy-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
  280. on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
  281. >
  282. </div>
  283. </div>
  284. <div
  285. class="language-{lang} rounded-t-lg -mt-8 {executing || stdout || stderr || result
  286. ? ''
  287. : 'rounded-b-lg'} overflow-hidden"
  288. >
  289. <div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
  290. <CodeEditor
  291. value={code}
  292. {id}
  293. {lang}
  294. on:save={() => {
  295. saveCode();
  296. }}
  297. on:change={(e) => {
  298. _code = e.detail.value;
  299. }}
  300. />
  301. </div>
  302. <div
  303. id="plt-canvas-{id}"
  304. class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
  305. />
  306. {#if executing}
  307. <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
  308. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  309. <div class="text-sm">Running...</div>
  310. </div>
  311. {:else if stdout || stderr || result}
  312. <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
  313. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  314. <div class="text-sm">{stdout || stderr || result}</div>
  315. </div>
  316. {/if}
  317. {/if}
  318. </div>
  319. </div>