CodeBlock.svelte 8.9 KB

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