code-playpad 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +560 -53
- package/dist/THIRD-PARTY-NOTICES.txt +50 -0
- package/dist/config-DZqcFPEP.js +5 -0
- package/dist/dist-BKRQwpg6.js +6 -0
- package/dist/{element-DtCQVStG.js → element-BYcX5Z6z.js} +11 -11
- package/dist/element.d.ts +3 -0
- package/dist/element.js +1 -1
- package/dist/hints-v5iHTd1l.js +6 -0
- package/dist/index.js +1 -1
- package/dist/java-1ISFU8hm.js +1 -0
- package/dist/java-DSEX5tzl.js +6 -0
- package/dist/javascript-BhISU47e.js +14 -0
- package/dist/javascript-hfFD1Fg2.js +9 -0
- package/dist/{pyodide-B8qzHgzU.js → pyodide-BTGQ_X_F.js} +8 -8
- package/dist/{pyodide-yv6x2HCQ.js → pyodide-j1wg1W9_.js} +8 -8
- package/dist/react.d.ts +4 -1
- package/dist/react.js +1 -1
- package/dist/runtime/hints.d.ts +11 -0
- package/dist/runtime/java-worker.generated.d.ts +8 -0
- package/dist/runtime/java.d.ts +3 -0
- package/dist/runtime/worker-runtime.d.ts +14 -4
- package/dist/standalone.js +11 -11
- package/dist/types.d.ts +6 -2
- package/dist/worker-runtime-CmbgEzLA.js +9 -0
- package/dist/worker-runtime-mfgjcG5l.js +4 -0
- package/package.json +11 -3
- package/dist/config-B2qK2rdk.js +0 -6
- package/dist/javascript-Cv8I8Gkw.js +0 -318
- package/dist/javascript-DGFKR69i.js +0 -323
- package/dist/worker-runtime-8Mnw4vAD.js +0 -9
- package/dist/worker-runtime-HpUlPmaH.js +0 -4
package/README.md
CHANGED
|
@@ -3,37 +3,134 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/code-playpad) [](./LICENSE)
|
|
4
4
|
|
|
5
5
|
Runnable code in any web page. A CodeMirror editor that **executes what it
|
|
6
|
-
shows** — Python, JavaScript and
|
|
7
|
-
No backend, no iframe, no sign-up.
|
|
6
|
+
shows** — Python, JavaScript, TypeScript and a documented subset of Java —
|
|
7
|
+
entirely in the reader's browser. No backend, no iframe, no sign-up.
|
|
8
8
|
|
|
9
9
|
**[Live demo →](https://code-playpad.swengr.com)**
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
|
-
<script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.
|
|
12
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.3.0"></script>
|
|
13
13
|
|
|
14
|
-
<code-playpad height="
|
|
14
|
+
<code-playpad height="auto">
|
|
15
15
|
print("hello from a static page")
|
|
16
16
|
</code-playpad>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
**Contents** — [Languages](#languages) · [Install](#install) · [Attributes](#attributes)
|
|
20
|
+
· [Sizing](#sizing) · [Several files](#several-files) · [Input](#input)
|
|
21
|
+
· [Controlling it from JavaScript](#controlling-it-from-javascript)
|
|
22
|
+
· [Events and grading](#events-and-grading) · [Saving work](#saving-work)
|
|
23
|
+
· [Light and dark](#light-and-dark) · [Styling](#styling) · [Framework recipes](#framework-recipes)
|
|
24
|
+
· [Production](#production) · [Worth knowing](#worth-knowing)
|
|
25
|
+
|
|
19
26
|
## Languages
|
|
20
27
|
|
|
21
28
|
| `language` | Runtime | Downloaded at runtime |
|
|
22
29
|
| --- | --- | --- |
|
|
23
|
-
| `python` (default) | Python 3.14 via Pyodide — full standard library, `input()`, real tracebacks, `numpy` on demand | ~
|
|
30
|
+
| `python` (default) | Python 3.14 via Pyodide — full standard library, `input()`, real tracebacks, `numpy` on demand | ~6 MB (13.5 MB unpacked), once, cached |
|
|
24
31
|
| `javascript` | The browser's own engine, in a Worker | nothing |
|
|
25
|
-
| `typescript` | Same engine, types stripped before running | ~
|
|
32
|
+
| `typescript` | Same engine, types stripped before running | ~70 KB, once, cached |
|
|
33
|
+
| `java` | A documented subset of Java 25, compiled and interpreted in a Worker by an engine built for this package — see [Java](#java) | nothing: the engine is a ~27 KB (gzipped) chunk of this package, loaded with the first Java widget |
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<code-playpad height="auto">print("python is the default")</code-playpad>
|
|
37
|
+
|
|
38
|
+
<code-playpad language="javascript" height="auto">
|
|
39
|
+
const doubled = [1, 2, 3].map((n) => n * 2);
|
|
40
|
+
console.log(doubled);
|
|
41
|
+
doubled.reduce((a, b) => a + b, 0)
|
|
42
|
+
</code-playpad>
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
<code-playpad language="typescript" height="auto">
|
|
45
|
+
type Point = { x: number; y: number };
|
|
46
|
+
const dist = (p: Point): number => Math.hypot(p.x, p.y);
|
|
47
|
+
dist({ x: 3, y: 4 })
|
|
48
|
+
</code-playpad>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A trailing expression prints its value, like a REPL. Each language loads **only
|
|
52
|
+
on pages that use it**: a Python-only page never downloads a byte of the
|
|
53
|
+
JavaScript runtime, and a JavaScript-only page never touches Pyodide's ~6 MB.
|
|
30
54
|
|
|
31
55
|
TypeScript here erases types, it does not check them — a type error will not
|
|
32
56
|
stop your code running. This is a playground, not `tsc`.
|
|
33
57
|
|
|
58
|
+
### Java
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<code-playpad language="java" height="auto">
|
|
62
|
+
int visits = 0;
|
|
63
|
+
|
|
64
|
+
void main() {
|
|
65
|
+
greet("Ada");
|
|
66
|
+
IO.println("greeted " + visits + " people");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void greet(String name) {
|
|
70
|
+
visits++;
|
|
71
|
+
IO.println("Hello, " + name + "!");
|
|
72
|
+
}
|
|
73
|
+
</code-playpad>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Java runs on an engine written for code-playpad, not on a JVM. It covers a
|
|
77
|
+
documented subset of Java 25 and checks its behaviour against a real JDK 25:
|
|
78
|
+
|
|
79
|
+
- Java 25 **compact source files** and instance `main`, or a classic
|
|
80
|
+
`public class Main` with `public static void main(String[] args)`
|
|
81
|
+
- `int`, `char`, `boolean`, `String`; one-dimensional arrays; static and
|
|
82
|
+
instance methods with overloading and recursion; fields; `if`/`while`/`for`,
|
|
83
|
+
`break`/`continue`/`return`; `String.length()`/`charAt()`/`equals()`;
|
|
84
|
+
`System.out`/`System.err` and `IO.print`/`IO.println`
|
|
85
|
+
- **Errors read like the JDK's**: compile errors in javac's format, exceptions
|
|
86
|
+
with Java stack traces and the JDK's NullPointerException messages
|
|
87
|
+
|
|
88
|
+
Anything outside the subset — `long`/`double`, `?:`, `switch`, objects and
|
|
89
|
+
constructors, collections, `Scanner`, imports — is reported as *not supported in
|
|
90
|
+
this version* rather than run with a different meaning. The file is `Main.java`
|
|
91
|
+
unless `entry` says otherwise. The engine stops runaway programs after 200
|
|
92
|
+
million steps and explains why.
|
|
93
|
+
|
|
94
|
+
## Download size
|
|
95
|
+
|
|
96
|
+
What a page downloads once a reader presses Run, with the CDN build. Measured by
|
|
97
|
+
loading one-widget pages in a browser and gzipping each file the page fetched;
|
|
98
|
+
CDN downloads are what the CDN actually sent.
|
|
99
|
+
|
|
100
|
+
| Page uses | code-playpad (gzip) | Also fetched from a CDN at run time | 0.2.1 (gzip) |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| Python | **165.8 KB** | Pyodide, about **6.0 MB** (a 3.3 MB wasm file and a 2.4 MB standard library) | 165.2 KB |
|
|
103
|
+
| JavaScript | **179.9 KB** | nothing | 180.5 KB |
|
|
104
|
+
| TypeScript | **179.9 KB** | Sucrase, 72 KB (≈ 252 KB in total) | ≈ 252.6 KB |
|
|
105
|
+
| Java | **189.7 KB** | nothing | n/a |
|
|
106
|
+
| All four | 247.2 KB | Pyodide and Sucrase | n/a |
|
|
107
|
+
|
|
108
|
+
- **A shared base of 144 KB is in every row:** the editor (CodeMirror) and the
|
|
109
|
+
widget, the same as 0.2.1 within 0.6 KB.
|
|
110
|
+
- **Each language adds:** Python 21.7 KB; JavaScript/TypeScript 35.8 KB; Java
|
|
111
|
+
45.6 KB, which is the 26.5 KB engine plus 19 KB of Java syntax highlighting.
|
|
112
|
+
- **Languages load lazily.** A Java page never fetches the Python or JavaScript
|
|
113
|
+
runtimes, and Python and JavaScript pages never fetch Java, so adding Java left
|
|
114
|
+
Python-only and JavaScript-only pages the same size as in 0.2.1.
|
|
115
|
+
- **Java is the lightest to run:** it is self-contained, while Python's first run
|
|
116
|
+
is dominated by the roughly 6 MB Pyodide download (cached after that).
|
|
117
|
+
- **With npm and a bundler** you already ship CodeMirror yourself. code-playpad's
|
|
118
|
+
own code then costs about 9 KB, plus the runtime chunk for each language used;
|
|
119
|
+
Java's is 27 KB, plus `@codemirror/lang-java` if you install it.
|
|
120
|
+
|
|
34
121
|
## Install
|
|
35
122
|
|
|
36
|
-
|
|
123
|
+
### From a CDN — nothing to install
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/code-playpad@0.3.0"></script>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This is the `standalone` build: CodeMirror is bundled in, so a static blog post
|
|
130
|
+
or a Markdown-rendered page needs only that one tag. Pin the version — an
|
|
131
|
+
unpinned URL follows `latest` and will change under your readers.
|
|
132
|
+
|
|
133
|
+
### From npm, with a bundler
|
|
37
134
|
|
|
38
135
|
```bash
|
|
39
136
|
npm install code-playpad @codemirror/state @codemirror/view @codemirror/commands \
|
|
@@ -41,59 +138,72 @@ npm install code-playpad @codemirror/state @codemirror/view @codemirror/commands
|
|
|
41
138
|
```
|
|
42
139
|
|
|
43
140
|
CodeMirror is a peer dependency so you never end up with two copies of
|
|
44
|
-
`@codemirror/state`. Language grammars
|
|
45
|
-
|
|
46
|
-
|
|
141
|
+
`@codemirror/state`. Language grammars are **optional** — install only what you
|
|
142
|
+
use (`@codemirror/lang-python`, `@codemirror/lang-javascript`,
|
|
143
|
+
`@codemirror/lang-json`, `@codemirror/lang-java`); a missing one degrades to
|
|
144
|
+
plain text rather than throwing.
|
|
47
145
|
|
|
48
146
|
```js
|
|
49
147
|
import 'code-playpad/element'; // registers <code-playpad>
|
|
50
148
|
```
|
|
51
149
|
|
|
52
|
-
|
|
53
|
-
|
|
150
|
+
If your bundler is configured to drop side-effect-only imports, register
|
|
151
|
+
explicitly instead:
|
|
54
152
|
|
|
55
153
|
```js
|
|
56
154
|
import { defineCodePlayPad } from 'code-playpad/element';
|
|
57
155
|
defineCodePlayPad();
|
|
58
156
|
```
|
|
59
157
|
|
|
60
|
-
|
|
158
|
+
### Entry points
|
|
61
159
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
height={180}
|
|
69
|
-
onOutput={(r) => setPassed(r.stdout.includes('hi'))}
|
|
70
|
-
/>
|
|
71
|
-
```
|
|
160
|
+
| Import | You get |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `code-playpad/element` | registers `<code-playpad>`; smallest thing that works |
|
|
163
|
+
| `code-playpad` | the same, plus every helper below |
|
|
164
|
+
| `code-playpad/react` | the `<CodePlayPad>` React component |
|
|
165
|
+
| `code-playpad/standalone` | CodeMirror bundled in — what the CDN serves |
|
|
72
166
|
|
|
73
|
-
Importing on a server is safe
|
|
74
|
-
|
|
167
|
+
Importing on a server is safe: without a DOM the element degrades to a no-op, so
|
|
168
|
+
Next.js, Astro and SvelteKit need no dynamic import or `client:only` guard.
|
|
75
169
|
|
|
76
|
-
##
|
|
170
|
+
## Attributes
|
|
77
171
|
|
|
78
172
|
| | |
|
|
79
173
|
| --- | --- |
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
174
|
+
| `language` | `python` (default), `javascript`, `typescript`, `java` |
|
|
175
|
+
| `height` | number (px), any CSS length, or `auto` |
|
|
176
|
+
| `min-height` / `max-height` | bounds for `auto` |
|
|
177
|
+
| `theme` | `light`, `dark`, or `auto` (default — follows the OS) |
|
|
178
|
+
| `readonly` | editor cannot be typed into; Run still works |
|
|
179
|
+
| `autorun` | runs once, on its own, when scrolled into view |
|
|
180
|
+
| `stdin` | shows the standard-input box up front |
|
|
181
|
+
| `persist` | key under which edits are saved (see [Saving work](#saving-work)) |
|
|
182
|
+
| `entry` | which file Run executes, for multi-file widgets |
|
|
183
|
+
|
|
184
|
+
```html
|
|
185
|
+
<!-- a worked example nobody should edit -->
|
|
186
|
+
<code-playpad height="auto" readonly>
|
|
187
|
+
print("read this, don't change it")
|
|
188
|
+
</code-playpad>
|
|
84
189
|
|
|
85
|
-
|
|
86
|
-
|
|
190
|
+
<!-- output visible before the reader does anything -->
|
|
191
|
+
<code-playpad height="auto" autorun>
|
|
192
|
+
print("this already ran")
|
|
193
|
+
</code-playpad>
|
|
87
194
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
195
|
+
<!-- always dark, regardless of the reader's OS setting -->
|
|
196
|
+
<code-playpad height="auto" theme="dark">
|
|
197
|
+
print("always dark")
|
|
198
|
+
</code-playpad>
|
|
93
199
|
```
|
|
94
200
|
|
|
95
|
-
|
|
96
|
-
|
|
201
|
+
`readonly` blocks typing but not execution, and the code can still be replaced
|
|
202
|
+
from JavaScript — useful for "show the answer" buttons.
|
|
203
|
+
|
|
204
|
+
Note it is `language`, not `lang`: `lang` is a standard HTML attribute for the
|
|
205
|
+
*human* language of an element's text, and putting `python` there confuses
|
|
206
|
+
screen readers.
|
|
97
207
|
|
|
98
208
|
## Sizing
|
|
99
209
|
|
|
@@ -125,11 +235,11 @@ length (`40vh`, `30em`).
|
|
|
125
235
|
|
|
126
236
|
## Several files
|
|
127
237
|
|
|
128
|
-
A widget can hold a small project. Relative imports, packages
|
|
129
|
-
|
|
238
|
+
A widget can hold a small project. Relative imports, packages and data files all
|
|
239
|
+
work, and tracebacks name the real file and line.
|
|
130
240
|
|
|
131
241
|
```html
|
|
132
|
-
<code-playpad entry="main.py" height="
|
|
242
|
+
<code-playpad entry="main.py" height="auto" max-height="420">
|
|
133
243
|
<playpad-file name="main.py">
|
|
134
244
|
from geometry import Circle
|
|
135
245
|
print(Circle(1).area())
|
|
@@ -149,9 +259,34 @@ all work, and tracebacks name the real file and line.
|
|
|
149
259
|
</code-playpad>
|
|
150
260
|
```
|
|
151
261
|
|
|
262
|
+
JavaScript and TypeScript work the same way, including `.json` imported as data:
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
<code-playpad language="javascript" entry="main.js" height="auto">
|
|
266
|
+
<playpad-file name="main.js">
|
|
267
|
+
import { total } from "./cart.js";
|
|
268
|
+
import items from "./items.json";
|
|
269
|
+
|
|
270
|
+
console.log(total(items));
|
|
271
|
+
</playpad-file>
|
|
272
|
+
|
|
273
|
+
<playpad-file name="cart.js">
|
|
274
|
+
export const total = (items) =>
|
|
275
|
+
items.reduce((sum, item) => sum + item.price, 0);
|
|
276
|
+
</playpad-file>
|
|
277
|
+
|
|
278
|
+
<playpad-file name="items.json">
|
|
279
|
+
[{ "price": 12 }, { "price": 30 }]
|
|
280
|
+
</playpad-file>
|
|
281
|
+
</code-playpad>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A tab bar appears once there is more than one file, with `▸` marking the entry
|
|
285
|
+
point, and each file keeps its own undo history. Editing a module takes effect
|
|
286
|
+
on the next run — the import cache is cleared between runs.
|
|
287
|
+
|
|
152
288
|
Highlighting follows the **file**, not the runtime: a `.json` beside your
|
|
153
289
|
JavaScript is highlighted as JSON, a `.csv` beside your Python is plain text.
|
|
154
|
-
`.json` files are importable as data, the same as in Node.
|
|
155
290
|
|
|
156
291
|
## Input
|
|
157
292
|
|
|
@@ -165,10 +300,299 @@ age? 36
|
|
|
165
300
|
hello Ada, next year you turn 37
|
|
166
301
|
```
|
|
167
302
|
|
|
168
|
-
`Enter` sends a line; **EOF** (or `Ctrl-D`) ends input
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
303
|
+
`Enter` sends a line; **EOF** (or `Ctrl-D`) ends input, which is how you finish
|
|
304
|
+
a `while True:` loop.
|
|
305
|
+
|
|
306
|
+
In JavaScript and TypeScript, `input()` is a global returning a promise, so
|
|
307
|
+
`await` works at the top level:
|
|
308
|
+
|
|
309
|
+
```html
|
|
310
|
+
<code-playpad language="javascript" height="auto">
|
|
311
|
+
const name = await input("your name? ");
|
|
312
|
+
console.log(`hello ${name}`);
|
|
313
|
+
</code-playpad>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
To script the answers instead of typing them — useful for a worked example —
|
|
317
|
+
pre-fill the box. Those lines are used before the reader is asked:
|
|
318
|
+
|
|
319
|
+
```html
|
|
320
|
+
<code-playpad height="auto" stdin>
|
|
321
|
+
name = input("name? ")
|
|
322
|
+
print("hi", name)
|
|
323
|
+
</code-playpad>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
document.querySelector('code-playpad').stdin = 'Ada\n36';
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Echoed input is display-only: `stdout` stays exactly what the program printed,
|
|
331
|
+
so graders are unaffected.
|
|
332
|
+
|
|
333
|
+
Java does not read input yet; `Scanner` over the input box is planned.
|
|
334
|
+
|
|
335
|
+
## Controlling it from JavaScript
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
const pad = document.querySelector('code-playpad');
|
|
339
|
+
|
|
340
|
+
pad.code = 'print("set from outside")'; // replace the visible file
|
|
341
|
+
const source = pad.getCode(); // read it back
|
|
342
|
+
const result = await pad.run(); // { stdout, stderr?, value?, durationMs }
|
|
343
|
+
pad.reset(); // back to the snippet the page shipped
|
|
344
|
+
pad.stop(); // stop this widget's run, queued or running
|
|
345
|
+
await pad.prewarm(); // boot the runtime before the reader runs anything
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Multi-file widgets add:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
pad.files = { // replace the whole project
|
|
352
|
+
'main.py': 'from util import shout\nprint(shout("hi"))',
|
|
353
|
+
'util.py': 'def shout(s): return s.upper() + "!"'
|
|
354
|
+
};
|
|
355
|
+
pad.entry = 'main.py'; // which file Run executes
|
|
356
|
+
pad.openFile('util.py'); // bring a tab on screen
|
|
357
|
+
const everything = pad.getFiles(); // { name: contents }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`prewarm()` is worth calling when you know the reader is about to run something
|
|
361
|
+
— on a lesson page, for instance — so the ~6 MB Python download starts before
|
|
362
|
+
they press Run rather than after.
|
|
363
|
+
|
|
364
|
+
## Events and grading
|
|
365
|
+
|
|
366
|
+
Four events bubble and cross the shadow boundary, so answer-checking lives
|
|
367
|
+
outside the widget rather than inside it:
|
|
368
|
+
|
|
369
|
+
| event | `event.detail` |
|
|
370
|
+
| --- | --- |
|
|
371
|
+
| `py-ready` | `{ language, label }` — runtime booted |
|
|
372
|
+
| `py-run` | `{ code, entry, files, language }` — a run started |
|
|
373
|
+
| `py-output` | `{ stdout, stderr?, value?, table?, durationMs, language }` |
|
|
374
|
+
| `py-error` | `{ message, hint?, language }` |
|
|
375
|
+
|
|
376
|
+
A complete exercise check:
|
|
377
|
+
|
|
378
|
+
```html
|
|
379
|
+
<code-playpad id="ex1" height="auto" min-height="150" persist="ex1">
|
|
380
|
+
# Print the numbers 1 to 5, one per line.
|
|
381
|
+
</code-playpad>
|
|
382
|
+
<p id="ex1-result"></p>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
const expected = '1\n2\n3\n4\n5\n';
|
|
387
|
+
|
|
388
|
+
document.addEventListener('py-output', (event) => {
|
|
389
|
+
if (event.target.id !== 'ex1') return;
|
|
390
|
+
|
|
391
|
+
const el = document.getElementById('ex1-result');
|
|
392
|
+
if (event.detail.stderr) {
|
|
393
|
+
el.textContent = 'Your code raised an error — read the message above.';
|
|
394
|
+
} else if (event.detail.stdout === expected) {
|
|
395
|
+
el.textContent = '✅ Correct';
|
|
396
|
+
} else {
|
|
397
|
+
el.textContent = `❌ Expected ${JSON.stringify(expected)}, got ${JSON.stringify(event.detail.stdout)}`;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`py-error` also carries a `hint` when the runtime can explain the failure in
|
|
403
|
+
plain language — for example when a program calls `input()` more times than the
|
|
404
|
+
standard-input box has lines.
|
|
405
|
+
|
|
406
|
+
## Saving work
|
|
407
|
+
|
|
408
|
+
`persist` stores the whole file map in IndexedDB, so a reader's edits survive
|
|
409
|
+
navigation *and* a full reload:
|
|
410
|
+
|
|
411
|
+
```html
|
|
412
|
+
<code-playpad height="auto" persist="lesson-3-exercise-1">
|
|
413
|
+
# Your code here
|
|
414
|
+
</code-playpad>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The page's file list stays authoritative: edits come back, but files you add or
|
|
418
|
+
remove later follow the page, not the old snapshot.
|
|
419
|
+
|
|
420
|
+
For a course you usually also want completion state, which the package exports:
|
|
421
|
+
|
|
422
|
+
```js
|
|
423
|
+
import { setProgress, getProgress, allProgress, loadSnippet, clearSnippet } from 'code-playpad';
|
|
424
|
+
|
|
425
|
+
// mark a lesson done when its exercise passes
|
|
426
|
+
document.addEventListener('py-output', async (event) => {
|
|
427
|
+
if (event.detail.stdout === expected) {
|
|
428
|
+
await setProgress('lesson-3', { completed: true, score: 100 });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await getProgress('lesson-3'); // { id, completed, score, updatedAt }
|
|
433
|
+
await allProgress(); // every record, for a progress bar
|
|
434
|
+
await loadSnippet('lesson-3-exercise-1'); // what the reader last wrote
|
|
435
|
+
await clearSnippet('lesson-3-exercise-1'); // a "start over" button
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
All of it is per-browser: nothing leaves the reader's machine.
|
|
439
|
+
|
|
440
|
+
## Light and dark
|
|
441
|
+
|
|
442
|
+
`theme` takes `light`, `dark`, or `auto` (the default).
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<code-playpad theme="dark" height="auto">print("always dark")</code-playpad>
|
|
446
|
+
<code-playpad theme="light" height="auto">print("always light")</code-playpad>
|
|
447
|
+
<code-playpad height="auto">print("follows the page, then the OS")</code-playpad>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**`auto` follows your site first, the operating system second.** If your page
|
|
451
|
+
has its own light/dark switch, the widget follows it as long as the switch sets
|
|
452
|
+
`color-scheme` on the root element — which is how most themes are written:
|
|
453
|
+
|
|
454
|
+
```css
|
|
455
|
+
:root { color-scheme: light; }
|
|
456
|
+
:root[data-theme="dark"] { color-scheme: dark; }
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Flip `data-theme` (or a class, or an inline style) and every widget on the page
|
|
460
|
+
re-themes itself. Nothing else to wire up. With no `color-scheme` on the page,
|
|
461
|
+
`auto` falls back to the reader's OS setting via `prefers-color-scheme`.
|
|
462
|
+
|
|
463
|
+
If your theme switch does *not* set `color-scheme`, drive the widgets directly:
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
function setTheme(mode) { // mode: 'light' | 'dark'
|
|
467
|
+
document.documentElement.dataset.theme = mode;
|
|
468
|
+
for (const pad of document.querySelectorAll('code-playpad')) {
|
|
469
|
+
pad.setAttribute('theme', mode);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
An explicit `theme` attribute always wins over the page, so a deliberately dark
|
|
475
|
+
snippet stays dark on a light page.
|
|
476
|
+
|
|
477
|
+
## Styling
|
|
478
|
+
|
|
479
|
+
The widget lives in a shadow root, so page CSS cannot leak in — and its own
|
|
480
|
+
styles cannot leak out. Two supported ways to restyle it.
|
|
481
|
+
|
|
482
|
+
**Custom properties** inherit through the shadow boundary:
|
|
483
|
+
|
|
484
|
+
```css
|
|
485
|
+
code-playpad {
|
|
486
|
+
--cp-accent: #b5179e; /* buttons, focus, the caret */
|
|
487
|
+
--cp-radius: 14px;
|
|
488
|
+
--cp-font-mono: "JetBrains Mono", monospace;
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| | |
|
|
493
|
+
| --- | --- |
|
|
494
|
+
| colours | `--cp-bg`, `--cp-fg`, `--cp-muted`, `--cp-border`, `--cp-surface` |
|
|
495
|
+
| accents | `--cp-accent`, `--cp-accent-fg`, `--cp-danger`, `--cp-ok` |
|
|
496
|
+
| shape and type | `--cp-radius`, `--cp-font-mono`, `--cp-font-ui` |
|
|
497
|
+
|
|
498
|
+
**Parts** expose the pieces worth targeting:
|
|
499
|
+
|
|
500
|
+
```css
|
|
501
|
+
code-playpad::part(run-button) { border-radius: 999px; }
|
|
502
|
+
code-playpad::part(stop-button) { font-weight: 700; }
|
|
503
|
+
code-playpad::part(output) { background: #fff7fb; }
|
|
504
|
+
code-playpad::part(tabs) { border-bottom-width: 2px; }
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Both work from an ordinary stylesheet, no `::shadow` hacks. For dark mode, the
|
|
508
|
+
`theme` attribute already follows the reader's OS by default.
|
|
509
|
+
|
|
510
|
+
## Framework recipes
|
|
511
|
+
|
|
512
|
+
**Astro** — put the import in a page or layout script; the element does the rest.
|
|
513
|
+
|
|
514
|
+
```astro
|
|
515
|
+
---
|
|
516
|
+
// src/layouts/Lesson.astro
|
|
517
|
+
---
|
|
518
|
+
<script>
|
|
519
|
+
import 'code-playpad/element';
|
|
520
|
+
</script>
|
|
521
|
+
|
|
522
|
+
<code-playpad height="auto" persist={Astro.props.id}>
|
|
523
|
+
{Astro.props.code}
|
|
524
|
+
</code-playpad>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**React** — the wrapper assigns props through a ref, so React 18 behaves like 19.
|
|
528
|
+
|
|
529
|
+
```jsx
|
|
530
|
+
import { useState } from 'react';
|
|
531
|
+
import { CodePlayPad } from 'code-playpad/react';
|
|
532
|
+
|
|
533
|
+
export function Exercise() {
|
|
534
|
+
const [passed, setPassed] = useState(false);
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<>
|
|
538
|
+
<CodePlayPad
|
|
539
|
+
language="python"
|
|
540
|
+
code={'# Your code here'}
|
|
541
|
+
height="auto"
|
|
542
|
+
minHeight={150}
|
|
543
|
+
persist="exercise-1"
|
|
544
|
+
onOutput={(r) => setPassed(r.stdout.trim() === '42')}
|
|
545
|
+
/>
|
|
546
|
+
{passed && <p>✅ Correct</p>}
|
|
547
|
+
</>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Props: `code`, `files`, `entry`, `language`, `height`, `stdin`, `theme`,
|
|
553
|
+
`readOnly`, `autoRun`, `persist`, `className`, `style`, and the callbacks
|
|
554
|
+
`onReady`, `onRun`, `onOutput`, `onError`.
|
|
555
|
+
|
|
556
|
+
**Next.js** — the element is SSR-safe, so a plain import works in a client
|
|
557
|
+
component:
|
|
558
|
+
|
|
559
|
+
```jsx
|
|
560
|
+
'use client';
|
|
561
|
+
import 'code-playpad/element';
|
|
562
|
+
|
|
563
|
+
export default function Lesson() {
|
|
564
|
+
return <code-playpad height="auto">{'print("hi")'}</code-playpad>;
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Vue** — tell the compiler the tag is a custom element, then use it directly:
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
// vite.config.js
|
|
572
|
+
export default {
|
|
573
|
+
plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'code-playpad' } } })]
|
|
574
|
+
};
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Plain HTML / Markdown** — the CDN tag plus the element. Because the code lives
|
|
578
|
+
in the element's text content, the snippet is still readable if JavaScript never
|
|
579
|
+
loads.
|
|
580
|
+
|
|
581
|
+
## Production
|
|
582
|
+
|
|
583
|
+
Serve Pyodide from your own origin so you control its cache headers, rather than
|
|
584
|
+
depending on a public CDN:
|
|
585
|
+
|
|
586
|
+
```js
|
|
587
|
+
import { configurePython, pyodideAssetUrls } from 'code-playpad';
|
|
588
|
+
|
|
589
|
+
configurePython({ indexURL: '/pyodide/' }); // before the first run
|
|
590
|
+
pyodideAssetUrls(); // the files worth <link rel=preload>-ing
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
`configureTypeScript({ sucraseURL })` does the same for the TypeScript
|
|
594
|
+
transform. Give those files a long `Cache-Control`: one page's download then
|
|
595
|
+
warms every other page on the site.
|
|
172
596
|
|
|
173
597
|
## Worth knowing
|
|
174
598
|
|
|
@@ -177,15 +601,93 @@ program printed, so graders are unaffected.
|
|
|
177
601
|
single-threaded, so a second widget waits for the first.
|
|
178
602
|
- **Runaway code cannot freeze the page.** Code runs in a Worker; **Stop**
|
|
179
603
|
terminates and reboots it. Runs queued behind the stopped one survive.
|
|
604
|
+
Stopping a widget that is still `Queued…` just takes it out of the queue —
|
|
605
|
+
whatever is running keeps running.
|
|
180
606
|
- **A program waiting on `input()` holds the shared runtime.** Other widgets
|
|
181
607
|
show `Queued…` until it finishes or is stopped.
|
|
182
608
|
- **Nothing boots on page load.** An IntersectionObserver starts a runtime as a
|
|
183
|
-
widget nears the viewport; touching the editor also wakes it.
|
|
609
|
+
widget nears the viewport; touching the editor also wakes it. Use `prewarm()`
|
|
610
|
+
to start earlier on purpose.
|
|
184
611
|
- **Python in the browser has limits**: no threads, no `multiprocessing`, no
|
|
185
612
|
sockets, no `subprocess`. `requests` works but goes through browser `fetch`,
|
|
186
613
|
so CORS applies.
|
|
187
|
-
- **
|
|
188
|
-
|
|
614
|
+
- **If CodeMirror fails to load**, the widget falls back to a plain textarea that
|
|
615
|
+
still runs code, rather than showing nothing.
|
|
616
|
+
|
|
617
|
+
## Version history
|
|
618
|
+
|
|
619
|
+
No breaking changes so far: markup and API written against 0.1.3 still work on
|
|
620
|
+
0.3.0.
|
|
621
|
+
|
|
622
|
+
### 0.3.0
|
|
623
|
+
|
|
624
|
+
- **`language="java"`**: a documented subset of Java 25 — see [Java](#java).
|
|
625
|
+
The engine is a ~27 KB (gzipped) chunk loaded only by pages with a Java
|
|
626
|
+
widget; Python and JavaScript pages download the same as in 0.2.1 (see
|
|
627
|
+
[Download size](#download-size)). `@codemirror/lang-java` is a new optional
|
|
628
|
+
peer dependency for its highlighting.
|
|
629
|
+
- **Fixed:** in JavaScript, a top-level `await` inside an expression
|
|
630
|
+
(`Number(await input())`) failed with `SyntaxError: missing ) after argument
|
|
631
|
+
list`; it only worked when the first `await` started a statement.
|
|
632
|
+
- A hint about something other than input (a runaway loop, say) no longer opens
|
|
633
|
+
the standard-input box; only "input() asked for a line…" does.
|
|
634
|
+
|
|
635
|
+
0.2.2 was never published; its fixes ship in 0.3.0:
|
|
636
|
+
|
|
637
|
+
- **Fixed:** pressing **Stop** on a widget still showing `Queued…` stopped a
|
|
638
|
+
*different* widget — whichever one was running — and the queued run then
|
|
639
|
+
went ahead anyway. Stop now cancels only the widget's own run: a queued run is
|
|
640
|
+
dropped without touching the worker, and only a run that is actually
|
|
641
|
+
executing kills and reboots it. `pad.stop()` on an idle widget no longer
|
|
642
|
+
affects anyone else.
|
|
643
|
+
- **Fixed:** stopping a run while the runtime was rebooting from an earlier
|
|
644
|
+
Stop could wedge the queue — every later run sat at `Queued…` forever.
|
|
645
|
+
- `Runtime.stop()` takes an optional argument: the promise `run()` returned,
|
|
646
|
+
to stop just that run. With no argument it behaves as before.
|
|
647
|
+
- **Fixed:** a line printed just before an infinite loop (`print("started")`
|
|
648
|
+
then `while True: pass`) never appeared — output batching held it back, and
|
|
649
|
+
the loop never let the worker send it. The first lines of any burst are now
|
|
650
|
+
sent immediately, in Python and JavaScript; large outputs are still batched.
|
|
651
|
+
- The JavaScript/TypeScript worker is now minified in `dist/` like the Python
|
|
652
|
+
one; it previously shipped as readable source, comments included. The
|
|
653
|
+
`javascript` chunk drops from about 12KB to about 7KB.
|
|
654
|
+
|
|
655
|
+
### 0.2.1
|
|
656
|
+
|
|
657
|
+
- `theme="auto"` now follows the **page** before the operating system: if your
|
|
658
|
+
site's light/dark switch sets `color-scheme` on the root, widgets re-theme
|
|
659
|
+
with it. Previously a page could go dark while every widget stayed light.
|
|
660
|
+
- `minHeight` and `maxHeight` props on the React wrapper — 0.2.0 added them as
|
|
661
|
+
attributes but missed the wrapper.
|
|
662
|
+
- README expanded into full usage documentation: entry points, an example per
|
|
663
|
+
attribute, programmatic control, a complete grading example, the progress
|
|
664
|
+
store, styling, and framework recipes.
|
|
665
|
+
|
|
666
|
+
### 0.2.0
|
|
667
|
+
|
|
668
|
+
- `height="auto"` is a supported content-fitting mode. It previously worked by
|
|
669
|
+
accident and could have broken silently.
|
|
670
|
+
- New `min-height` and `max-height` attributes. `max-height` scrolls rather than
|
|
671
|
+
clipping; `min-height` grows the editor itself, so the empty space below a
|
|
672
|
+
short snippet is still clickable — what an exercise answer box needs.
|
|
673
|
+
|
|
674
|
+
### 0.1.4
|
|
675
|
+
|
|
676
|
+
- **Fixed:** a `<pre>` or other wrapper on its own line leaked its indentation
|
|
677
|
+
into the program, so two-line snippets failed with `IndentationError` while
|
|
678
|
+
single-line ones looked fine.
|
|
679
|
+
- **Fixed:** `sideEffects` named the re-export shims rather than the chunk that
|
|
680
|
+
registers the element, so a bundler could legitimately drop it and
|
|
681
|
+
`<code-playpad>` would never register in a production build.
|
|
682
|
+
- No runtime dependencies (an analytics package used only by the demo had crept
|
|
683
|
+
into `dependencies`).
|
|
684
|
+
|
|
685
|
+
### 0.1.3
|
|
686
|
+
|
|
687
|
+
- Relicensed **MIT**. Earlier versions were published as `UNLICENSED`, which
|
|
688
|
+
granted nobody the right to use them.
|
|
689
|
+
|
|
690
|
+
Versions 0.1.0 to 0.1.2 were withdrawn and are not installable.
|
|
189
691
|
|
|
190
692
|
## Licence
|
|
191
693
|
|
|
@@ -196,3 +698,8 @@ it, modify it. Bundled third-party components (CodeMirror 6, Lezer) remain under
|
|
|
196
698
|
their own MIT licences, reproduced in full in `dist/THIRD-PARTY-NOTICES.txt`.
|
|
197
699
|
Pyodide (MPL-2.0) is fetched from a CDN at runtime and is not redistributed by
|
|
198
700
|
this package.
|
|
701
|
+
|
|
702
|
+
The Java engine is original code under the same MIT licence. Java is a trademark
|
|
703
|
+
of Oracle and/or its affiliates; code-playpad is not affiliated with, endorsed
|
|
704
|
+
by, or certified by Oracle, and its Java support is a documented subset, not a
|
|
705
|
+
Java implementation.
|