CodeBlock.svelte 8.2 KB

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