11import hljs from "highlight.js/lib/core" ;
2+ import python from "highlight.js/lib/languages/python" ;
23import yaml from "highlight.js/lib/languages/yaml" ;
34import { computed , onBeforeUnmount , onMounted , ref , watch } from "vue" ;
45
56hljs . registerLanguage ( "yaml" , yaml ) ;
7+ hljs . registerLanguage ( "python" , python ) ;
68
79type Theme = "light" | "dark" ;
810
@@ -11,7 +13,6 @@ type Tone = "muted" | "ready" | "warn";
1113type SiteConfig = {
1214 pyodide_version : string ;
1315 wheel_file : string ;
14- wheel_version ?: string ;
1516} ;
1617
1718type PyodideRuntime = {
@@ -66,6 +67,29 @@ function escapeHtml(text: string): string {
6667 return text . replaceAll ( "&" , "&" ) . replaceAll ( "<" , "<" ) . replaceAll ( ">" , ">" ) ;
6768}
6869
70+ async function writeClipboardText ( text : string ) : Promise < void > {
71+ try {
72+ await navigator . clipboard . writeText ( text ) ;
73+ return ;
74+ } catch {
75+ const helper = document . createElement ( "textarea" ) ;
76+ helper . value = text ;
77+ helper . style . position = "fixed" ;
78+ helper . style . left = "-9999px" ;
79+ helper . style . top = "0" ;
80+ helper . setAttribute ( "readonly" , "" ) ;
81+ document . body . append ( helper ) ;
82+ helper . focus ( ) ;
83+ helper . select ( ) ;
84+ helper . setSelectionRange ( 0 , helper . value . length ) ;
85+ const copied = document . execCommand ( "copy" ) ;
86+ helper . remove ( ) ;
87+ if ( ! copied ) {
88+ throw new Error ( "Copy command failed" ) ;
89+ }
90+ }
91+ }
92+
6993function parseSimpleToml ( source : string ) : SiteConfig {
7094 const entries = Object . fromEntries (
7195 Array . from ( source . matchAll ( / ^ \s * ( [ a - z _ ] + ) \s * = \s * " ( [ ^ " ] * ) " \s * $ / gim) , ( [ , key , value ] ) => [
@@ -83,7 +107,6 @@ function parseSimpleToml(source: string): SiteConfig {
83107 return {
84108 pyodide_version : pyodideVersion ,
85109 wheel_file : wheelFile ,
86- wheel_version : entries . wheel_version ,
87110 } ;
88111}
89112
@@ -179,6 +202,7 @@ export function usePlayground() {
179202 const theme = ref < Theme > ( "dark" ) ;
180203 const yamlInput = ref ( DEFAULT_YAML ) ;
181204 const output = ref ( "" ) ;
205+ const outputHighlight = ref ( "" ) ;
182206 const busyLabel = ref ( "Loading runtime" ) ;
183207 const busy = ref ( false ) ;
184208 const renderError = ref ( false ) ;
@@ -207,7 +231,7 @@ export function usePlayground() {
207231 let lastLineCount = 0 ;
208232 let dragging = false ;
209233 let renderQueuedSource = "" ;
210- const copyTimers : Record < "yaml" | "output" , number > = { yaml : 0 , output : 0 } ;
234+ const feedbackTimers : Record < "yaml" | "output" , number > = { yaml : 0 , output : 0 } ;
211235
212236 function setBusy ( active : boolean , label = "Parsing YAML" ) : void {
213237 busy . value = active ;
@@ -252,6 +276,27 @@ export function usePlayground() {
252276 }
253277 }
254278
279+ function renderOutputHighlight ( source : string ) : void {
280+ if ( renderError . value ) {
281+ outputHighlight . value = escapeHtml ( source ) ;
282+ return ;
283+ }
284+
285+ if ( source . length > HLJS_MAX_LENGTH ) {
286+ outputHighlight . value = escapeHtml ( source ) ;
287+ return ;
288+ }
289+
290+ try {
291+ outputHighlight . value = hljs . highlight ( source , {
292+ language : "python" ,
293+ ignoreIllegals : true ,
294+ } ) . value ;
295+ } catch {
296+ outputHighlight . value = escapeHtml ( source ) ;
297+ }
298+ }
299+
255300 function syncYamlScroll ( ) : void {
256301 if ( ! inputRef . value || ! highlightRef . value || ! lineNumbersRef . value ) {
257302 return ;
@@ -292,13 +337,15 @@ export function usePlayground() {
292337 }
293338 renderError . value = false ;
294339 output . value = result ;
340+ renderOutputHighlight ( result ) ;
295341 engineTone . value = "ready" ;
296342 } catch ( error ) {
297343 if ( seq !== renderSeq ) {
298344 return ;
299345 }
300346 renderError . value = true ;
301347 output . value = String ( error ) ;
348+ renderOutputHighlight ( String ( error ) ) ;
302349 engineBadge . value = "Runtime error" ;
303350 engineTone . value = "warn" ;
304351 } finally {
@@ -348,6 +395,7 @@ await micropip.install("./wheels/${config.wheel_file}")
348395 } catch ( error ) {
349396 renderError . value = true ;
350397 output . value = String ( error ) ;
398+ renderOutputHighlight ( String ( error ) ) ;
351399 engineBadge . value = "Boot failed" ;
352400 engineTone . value = "warn" ;
353401 } finally {
@@ -373,9 +421,9 @@ await micropip.install("./wheels/${config.wheel_file}")
373421 applyTheme ( theme . value ) ;
374422 }
375423
376- function flashCopied ( kind : "yaml" | "output" ) : void {
377- if ( copyTimers [ kind ] ) {
378- window . clearTimeout ( copyTimers [ kind ] ) ;
424+ function flashFeedback ( kind : "yaml" | "output" ) : void {
425+ if ( feedbackTimers [ kind ] ) {
426+ window . clearTimeout ( feedbackTimers [ kind ] ) ;
379427 }
380428
381429 if ( kind === "yaml" ) {
@@ -384,7 +432,7 @@ await micropip.install("./wheels/${config.wheel_file}")
384432 outputCopied . value = true ;
385433 }
386434
387- copyTimers [ kind ] = window . setTimeout ( ( ) => {
435+ feedbackTimers [ kind ] = window . setTimeout ( ( ) => {
388436 if ( kind === "yaml" ) {
389437 yamlCopied . value = false ;
390438 } else {
@@ -394,30 +442,29 @@ await micropip.install("./wheels/${config.wheel_file}")
394442 }
395443
396444 async function copyText ( text : string , kind : "yaml" | "output" ) : Promise < void > {
397- try {
398- await navigator . clipboard . writeText ( text ) ;
399- flashCopied ( kind ) ;
445+ await writeClipboardText ( text ) ;
446+ flashFeedback ( kind ) ;
447+ }
448+
449+ const onPointerMove = ( event : PointerEvent ) => {
450+ if ( ! dragging ) {
400451 return ;
401- } catch {
402- const helper = document . createElement ( "textarea" ) ;
403- helper . value = text ;
404- helper . style . position = "fixed" ;
405- helper . style . left = "-9999px" ;
406- helper . style . top = "0" ;
407- helper . setAttribute ( "readonly" , "" ) ;
408- document . body . append ( helper ) ;
409- helper . focus ( ) ;
410- helper . select ( ) ;
411- helper . setSelectionRange ( 0 , helper . value . length ) ;
412- const copied = document . execCommand ( "copy" ) ;
413- helper . remove ( ) ;
414- if ( ! copied ) {
415- throw new Error ( "Copy command failed" ) ;
416- }
417452 }
453+ setSplitFromPointer ( event . clientX , event . clientY ) ;
454+ } ;
418455
419- flashCopied ( kind ) ;
420- }
456+ const onPointerUp = ( ) => {
457+ if ( ! dragging ) {
458+ return ;
459+ }
460+ dragging = false ;
461+ document . body . classList . remove ( "is-resizing" ) ;
462+ document . body . style . userSelect = "" ;
463+ } ;
464+
465+ const onResize = ( ) => {
466+ restoreSplitRatio ( ) ;
467+ } ;
421468
422469 watch ( yamlInput , ( ) => {
423470 scheduleHighlight ( ) ;
@@ -434,41 +481,23 @@ await micropip.install("./wheels/${config.wheel_file}")
434481 scheduleHighlight ( ) ;
435482 void boot ( ) ;
436483
437- const onPointerMove = ( event : PointerEvent ) => {
438- if ( ! dragging ) {
439- return ;
440- }
441- setSplitFromPointer ( event . clientX , event . clientY ) ;
442- } ;
443- const onPointerUp = ( ) => {
444- if ( ! dragging ) {
445- return ;
446- }
447- dragging = false ;
448- document . body . classList . remove ( "is-resizing" ) ;
449- document . body . style . userSelect = "" ;
450- } ;
451- const onResize = ( ) => {
452- restoreSplitRatio ( ) ;
453- } ;
454-
455484 window . addEventListener ( "pointermove" , onPointerMove ) ;
456485 window . addEventListener ( "pointerup" , onPointerUp ) ;
457486 window . addEventListener ( "pointercancel" , onPointerUp ) ;
458487 window . addEventListener ( "resize" , onResize , { passive : true } ) ;
488+ } ) ;
459489
460- onBeforeUnmount ( ( ) => {
461- window . removeEventListener ( "pointermove" , onPointerMove ) ;
462- window . removeEventListener ( "pointerup" , onPointerUp ) ;
463- window . removeEventListener ( "pointercancel" , onPointerUp ) ;
464- window . removeEventListener ( "resize" , onResize ) ;
465- window . clearTimeout ( renderTimer ) ;
466- window . clearTimeout ( copyTimers . yaml ) ;
467- window . clearTimeout ( copyTimers . output ) ;
468- if ( highlightFrame ) {
469- window . cancelAnimationFrame ( highlightFrame ) ;
470- }
471- } ) ;
490+ onBeforeUnmount ( ( ) => {
491+ window . removeEventListener ( "pointermove" , onPointerMove ) ;
492+ window . removeEventListener ( "pointerup" , onPointerUp ) ;
493+ window . removeEventListener ( "pointercancel" , onPointerUp ) ;
494+ window . removeEventListener ( "resize" , onResize ) ;
495+ window . clearTimeout ( renderTimer ) ;
496+ window . clearTimeout ( feedbackTimers . yaml ) ;
497+ window . clearTimeout ( feedbackTimers . output ) ;
498+ if ( highlightFrame ) {
499+ window . cancelAnimationFrame ( highlightFrame ) ;
500+ }
472501 } ) ;
473502
474503 return {
@@ -484,6 +513,7 @@ await micropip.install("./wheels/${config.wheel_file}")
484513 inputRef,
485514 lineNumbersRef,
486515 output,
516+ outputHighlight,
487517 outputCopied,
488518 renderError,
489519 setSplitFromPointer,
0 commit comments