Skip to content

Commit 5a69c53

Browse files
committed
refactor: move CopyButton to a dedicated directory, enhance output highlighting, and improve clipboard handling
1 parent df28380 commit 5a69c53

4 files changed

Lines changed: 152 additions & 106 deletions

File tree

site/src/App.vue

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import CopyButton from "./CopyButton.vue";
2+
import CopyButton from "./components/buttons/CopyButton.vue";
33
import { usePlayground } from "./playground";
44
55
const {
@@ -15,6 +15,7 @@ const {
1515
inputRef,
1616
lineNumbersRef,
1717
output,
18+
outputHighlight,
1819
outputCopied,
1920
renderError,
2021
startDrag,
@@ -132,12 +133,14 @@ const {
132133
<article class="panel">
133134
<header class="panel-head">
134135
<h2>YAML v1.2</h2>
135-
<CopyButton
136-
:copied="yamlCopied"
137-
label="Copy YAML"
138-
title="Copy YAML"
139-
@click="copyText(yamlInput, 'yaml')"
140-
/>
136+
<div class="panel-actions">
137+
<CopyButton
138+
:copied="yamlCopied"
139+
label="Copy YAML"
140+
title="Copy YAML"
141+
@click="copyText(yamlInput, 'yaml')"
142+
/>
143+
</div>
141144
</header>
142145

143146
<div class="editor-wrap">
@@ -170,16 +173,23 @@ const {
170173
<article class="panel panel-output" :class="{ 'is-busy': busy }">
171174
<header class="panel-head">
172175
<h2>Python</h2>
173-
<CopyButton
174-
:copied="outputCopied"
175-
label="Copy output"
176-
title="Copy output"
177-
@click="copyText(output, 'output')"
178-
/>
176+
<div class="panel-actions">
177+
<span class="panel-actions-spacer" aria-hidden="true"></span>
178+
<CopyButton
179+
:copied="outputCopied"
180+
label="Copy output"
181+
title="Copy output"
182+
@click="copyText(output, 'output')"
183+
/>
184+
</div>
179185
</header>
180186

181187
<div class="output-wrap">
182-
<pre :class="{ err: renderError }">{{ output }}</pre>
188+
<pre
189+
v-if="renderError"
190+
:class="{ err: renderError }"
191+
>{{ output }}</pre>
192+
<pre v-else class="python-highlight" v-html="outputHighlight"></pre>
183193
<div class="py-status" aria-live="polite" :aria-hidden="busy ? 'false' : 'true'">
184194
<span class="py-spinner" aria-hidden="true"></span>
185195
<span>{{ busyLabel }}</span>
File renamed without changes.

site/src/playground.ts

Lines changed: 88 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import hljs from "highlight.js/lib/core";
2+
import python from "highlight.js/lib/languages/python";
23
import yaml from "highlight.js/lib/languages/yaml";
34
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
45

56
hljs.registerLanguage("yaml", yaml);
7+
hljs.registerLanguage("python", python);
68

79
type Theme = "light" | "dark";
810

@@ -11,7 +13,6 @@ type Tone = "muted" | "ready" | "warn";
1113
type SiteConfig = {
1214
pyodide_version: string;
1315
wheel_file: string;
14-
wheel_version?: string;
1516
};
1617

1718
type PyodideRuntime = {
@@ -66,6 +67,29 @@ function escapeHtml(text: string): string {
6667
return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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+
6993
function 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

Comments
 (0)