CodeBlock.svelte 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <script lang="ts">
  2. import hljs from 'highlight.js';
  3. import { loadPyodide } from 'pyodide';
  4. import mermaid from 'mermaid';
  5. import { getContext, getAllContexts, onMount } from 'svelte';
  6. import { copyToClipboard } from '$lib/utils';
  7. import 'highlight.js/styles/github-dark.min.css';
  8. import PyodideWorker from '$lib/workers/pyodide.worker?worker';
  9. const i18n = getContext('i18n');
  10. export let id = '';
  11. export let token;
  12. export let lang = '';
  13. export let code = '';
  14. let mermaidHtml = null;
  15. let highlightedCode = null;
  16. let executing = false;
  17. let stdout = null;
  18. let stderr = null;
  19. let result = null;
  20. let copied = false;
  21. const copyCode = async () => {
  22. copied = true;
  23. await copyToClipboard(code);
  24. setTimeout(() => {
  25. copied = false;
  26. }, 1000);
  27. };
  28. const checkPythonCode = (str) => {
  29. // Check if the string contains typical Python syntax characters
  30. const pythonSyntax = [
  31. 'def ',
  32. 'else:',
  33. 'elif ',
  34. 'try:',
  35. 'except:',
  36. 'finally:',
  37. 'yield ',
  38. 'lambda ',
  39. 'assert ',
  40. 'nonlocal ',
  41. 'del ',
  42. 'True',
  43. 'False',
  44. 'None',
  45. ' and ',
  46. ' or ',
  47. ' not ',
  48. ' in ',
  49. ' is ',
  50. ' with '
  51. ];
  52. for (let syntax of pythonSyntax) {
  53. if (str.includes(syntax)) {
  54. return true;
  55. }
  56. }
  57. // If none of the above conditions met, it's probably not Python code
  58. return false;
  59. };
  60. const executePython = async (code) => {
  61. if (!code.includes('input') && !code.includes('matplotlib')) {
  62. executePythonAsWorker(code);
  63. } else {
  64. result = null;
  65. stdout = null;
  66. stderr = null;
  67. executing = true;
  68. document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`);
  69. let pyodide = await loadPyodide({
  70. indexURL: '/pyodide/',
  71. stdout: (text) => {
  72. console.log('Python output:', text);
  73. if (stdout) {
  74. stdout += `${text}\n`;
  75. } else {
  76. stdout = `${text}\n`;
  77. }
  78. },
  79. stderr: (text) => {
  80. console.log('An error occured:', text);
  81. if (stderr) {
  82. stderr += `${text}\n`;
  83. } else {
  84. stderr = `${text}\n`;
  85. }
  86. },
  87. packages: ['micropip']
  88. });
  89. try {
  90. const micropip = pyodide.pyimport('micropip');
  91. // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
  92. let packages = [
  93. code.includes('requests') ? 'requests' : null,
  94. code.includes('bs4') ? 'beautifulsoup4' : null,
  95. code.includes('numpy') ? 'numpy' : null,
  96. code.includes('pandas') ? 'pandas' : null,
  97. code.includes('matplotlib') ? 'matplotlib' : null,
  98. code.includes('sklearn') ? 'scikit-learn' : null,
  99. code.includes('scipy') ? 'scipy' : null,
  100. code.includes('re') ? 'regex' : null,
  101. code.includes('seaborn') ? 'seaborn' : null
  102. ].filter(Boolean);
  103. console.log(packages);
  104. await micropip.install(packages);
  105. result = await pyodide.runPythonAsync(`from js import prompt
  106. def input(p):
  107. return prompt(p)
  108. __builtins__.input = input`);
  109. result = await pyodide.runPython(code);
  110. if (!result) {
  111. result = '[NO OUTPUT]';
  112. }
  113. console.log(result);
  114. console.log(stdout);
  115. console.log(stderr);
  116. const pltCanvasElement = document.getElementById(`plt-canvas-${id}`);
  117. if (pltCanvasElement?.innerHTML !== '') {
  118. pltCanvasElement.classList.add('pt-4');
  119. }
  120. } catch (error) {
  121. console.error('Error:', error);
  122. stderr = error;
  123. }
  124. executing = false;
  125. }
  126. };
  127. const executePythonAsWorker = async (code) => {
  128. result = null;
  129. stdout = null;
  130. stderr = null;
  131. executing = true;
  132. let packages = [
  133. code.includes('requests') ? 'requests' : null,
  134. code.includes('bs4') ? 'beautifulsoup4' : null,
  135. code.includes('numpy') ? 'numpy' : null,
  136. code.includes('pandas') ? 'pandas' : null,
  137. code.includes('sklearn') ? 'scikit-learn' : null,
  138. code.includes('scipy') ? 'scipy' : null,
  139. code.includes('re') ? 'regex' : null,
  140. code.includes('seaborn') ? 'seaborn' : null
  141. ].filter(Boolean);
  142. console.log(packages);
  143. const pyodideWorker = new PyodideWorker();
  144. pyodideWorker.postMessage({
  145. id: id,
  146. code: code,
  147. packages: packages
  148. });
  149. setTimeout(() => {
  150. if (executing) {
  151. executing = false;
  152. stderr = 'Execution Time Limit Exceeded';
  153. pyodideWorker.terminate();
  154. }
  155. }, 60000);
  156. pyodideWorker.onmessage = (event) => {
  157. console.log('pyodideWorker.onmessage', event);
  158. const { id, ...data } = event.data;
  159. console.log(id, data);
  160. data['stdout'] && (stdout = data['stdout']);
  161. data['stderr'] && (stderr = data['stderr']);
  162. data['result'] && (result = data['result']);
  163. executing = false;
  164. };
  165. pyodideWorker.onerror = (event) => {
  166. console.log('pyodideWorker.onerror', event);
  167. executing = false;
  168. };
  169. };
  170. let debounceTimeout;
  171. $: if (code) {
  172. if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
  173. (async () => {
  174. try {
  175. const { svg } = await mermaid.render(`mermaid-${id}`, code);
  176. mermaidHtml = svg;
  177. } catch (error) {
  178. console.error('Error:', error);
  179. }
  180. })();
  181. } else {
  182. // Function to perform the code highlighting
  183. const highlightCode = () => {
  184. highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
  185. };
  186. // Clear the previous timeout if it exists
  187. clearTimeout(debounceTimeout);
  188. // Set a new timeout to debounce the code highlighting
  189. debounceTimeout = setTimeout(highlightCode, 10);
  190. }
  191. }
  192. onMount(async () => {
  193. await mermaid.initialize({ startOnLoad: true });
  194. if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
  195. try {
  196. const { svg } = await mermaid.render(`mermaid-${id}`, code);
  197. mermaidHtml = svg;
  198. } catch (error) {
  199. console.error('Error:', error);
  200. }
  201. }
  202. });
  203. </script>
  204. <div class="my-2" dir="ltr">
  205. {#if lang === 'mermaid'}
  206. {#if mermaidHtml}
  207. {@html mermaidHtml}
  208. {:else}
  209. <pre class=" mermaid-{id}">{code}</pre>
  210. {/if}
  211. {:else}
  212. <div
  213. class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
  214. >
  215. <div class="p-1">{@html lang}</div>
  216. <div class="flex items-center">
  217. {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
  218. {#if executing}
  219. <div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
  220. {:else}
  221. <button
  222. class="copy-code-button bg-none border-none p-1"
  223. on:click={() => {
  224. executePython(code);
  225. }}>{$i18n.t('Run')}</button
  226. >
  227. {/if}
  228. {/if}
  229. <button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
  230. >{copied ? $i18n.t('Copied') : $i18n.t('Copy Code')}</button
  231. >
  232. </div>
  233. </div>
  234. <pre
  235. class=" hljs p-4 px-5 overflow-x-auto"
  236. style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
  237. stdout ||
  238. stderr ||
  239. result) &&
  240. 'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
  241. class="language-{lang} rounded-t-none whitespace-pre"
  242. >{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
  243. ></pre>
  244. <div
  245. id="plt-canvas-{id}"
  246. class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
  247. />
  248. {#if executing}
  249. <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
  250. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  251. <div class="text-sm">Running...</div>
  252. </div>
  253. {:else if stdout || stderr || result}
  254. <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
  255. <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
  256. <div class="text-sm">{stdout || stderr || result}</div>
  257. </div>
  258. {/if}
  259. {/if}
  260. </div>