CodeBlock.svelte 7.6 KB

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