+"
+`;
+
+exports[`remarkDirectivePyide > should transform pyide with canvas attribute 1`] = `
+"
+
+
+
+
pyide-canvas
+
+
+
+
+
pyide-output
+
+
+
+
+
+ import pygame
+pygame.init()
+
+
+
+"
+`;
+
+exports[`remarkDirectivePyide > should transform pyide with packages attribute 1`] = `
+"
+
+
+
+
+
+
+
+
+ import snowballstemmer
+
+
+
+"
+`;
diff --git a/packages/markdown/tests/remarkDirectivePyide.test.ts b/packages/markdown/tests/remarkDirectivePyide.test.ts
new file mode 100644
index 000000000..abc961395
--- /dev/null
+++ b/packages/markdown/tests/remarkDirectivePyide.test.ts
@@ -0,0 +1,84 @@
+import { HyperbookContext } from "@hyperbook/types";
+import { describe, expect, it } from "vitest";
+import rehypeStringify from "rehype-stringify";
+import remarkToRehype from "remark-rehype";
+import rehypeFormat from "rehype-format";
+import { unified, PluggableList } from "unified";
+import remarkDirective from "remark-directive";
+import remarkDirectiveRehype from "remark-directive-rehype";
+import remarkDirectivePyide from "../src/remarkDirectivePyide";
+import { ctx } from "./mock";
+import remarkParse from "../src/remarkParse";
+
+export const toHtml = (md: string, ctx: HyperbookContext) => {
+ const remarkPlugins: PluggableList = [
+ remarkDirective,
+ remarkDirectiveRehype,
+ remarkDirectivePyide(ctx),
+ ];
+
+ return unified()
+ .use(remarkParse)
+ .use(remarkPlugins)
+ .use(remarkToRehype)
+ .use(rehypeFormat)
+ .use(rehypeStringify, {
+ allowDangerousCharacters: true,
+ allowDangerousHtml: true,
+ })
+ .processSync(md);
+};
+
+describe("remarkDirectivePyide", () => {
+ it("should transform basic pyide", async () => {
+ expect(
+ toHtml(
+ `:::pyide
+
+\`\`\`python
+print("Hello World")
+\`\`\`
+
+:::
+
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+
+ it("should transform pyide with canvas attribute", async () => {
+ expect(
+ toHtml(
+ `:::pyide{canvas}
+
+\`\`\`python
+import pygame
+pygame.init()
+\`\`\`
+
+:::
+
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+
+ it("should transform pyide with packages attribute", async () => {
+ expect(
+ toHtml(
+ `:::pyide{packages="snowballstemmer, nltk"}
+
+\`\`\`python
+import snowballstemmer
+\`\`\`
+
+:::
+
+`,
+ ctx,
+ ).value,
+ ).toMatchSnapshot();
+ });
+});
diff --git a/platforms/vscode/schemas/hyperbook.schema.json b/platforms/vscode/schemas/hyperbook.schema.json
index a0f46268f..7956991fc 100644
--- a/platforms/vscode/schemas/hyperbook.schema.json
+++ b/platforms/vscode/schemas/hyperbook.schema.json
@@ -361,6 +361,15 @@
},
"trailingSlash": {
"type": "boolean"
+ },
+ "version": {
+ "description": "Controls how the Hyperbook version is displayed.\n- \"text\": Shows version below the \"Powered by Hyperbook\" label.\n- \"tooltip\": Shows version on hover of the \"Powered by Hyperbook\" label.\n- \"console\": Outputs version as ASCII art in the browser console (default).",
+ "enum": [
+ "console",
+ "text",
+ "tooltip"
+ ],
+ "type": "string"
}
},
"required": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5ade5837..16e4e0231 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -376,8 +376,8 @@ importers:
specifier: ^1.1.5
version: 1.1.5
'@webcoder49/code-input':
- specifier: ^2.2.1
- version: 2.2.1
+ specifier: ^2.8.2
+ version: 2.8.2
abcjs:
specifier: ^6.6.0
version: 6.6.0
@@ -3001,8 +3001,8 @@ packages:
'@webassemblyjs/wast-printer@1.14.1':
resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==}
- '@webcoder49/code-input@2.2.1':
- resolution: {integrity: sha512-1B+wLZhBR1wyuD1bUICgNZNy5VA/z1YReMmsaGvdEHaV/Jfc7EB/4wnQ+78vEfxx0SZvHyosCivAmfmaq6tIHg==}
+ '@webcoder49/code-input@2.8.2':
+ resolution: {integrity: sha512-ji7qsSxqCFtoHgVBerwb/TZFT/5TPSEUtVXQzyQpxupWhS/Fsz3nzGdZ2T+26HbnDJDovckOGoHrC7a3ogY2rw==}
'@webpack-cli/configtest@3.0.1':
resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==}
@@ -11545,7 +11545,7 @@ snapshots:
'@webassemblyjs/ast': 1.14.1
'@xtuc/long': 4.2.2
- '@webcoder49/code-input@2.2.1': {}
+ '@webcoder49/code-input@2.8.2': {}
'@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.97.1)':
dependencies:
diff --git a/website/de/book/elements/pyide.md b/website/de/book/elements/pyide.md
index 269794c57..c7c7eac3f 100644
--- a/website/de/book/elements/pyide.md
+++ b/website/de/book/elements/pyide.md
@@ -30,6 +30,20 @@ print(a)
Sie können auch jedes Paket verwenden, das hier aufgeführt ist: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
+Wenn Sie Pakete aus PyPI benötigen, verwenden Sie das Attribut `packages` mit einer kommaseparierten Liste. Hyperbook lädt `micropip` und installiert diese Pakete vor der Ausführung Ihres Skripts.
+
+````md
+:::pyide{packages="snowballstemmer"}
+
+```python
+import snowballstemmer
+stemmer = snowballstemmer.stemmer("english")
+print(stemmer.stemWords(["running", "runner", "runs"]))
+```
+
+:::
+````
+
````md
:::pyide
@@ -114,27 +128,14 @@ def check_palindrom(s):
:::
-## Input()
-
-Sie können die `input()`-Funktion in den Code-Snippets verwenden. Die `input()`-Funktion wird durch die im `input`-Tag angegebenen Werte ersetzt.
-Wenn es mehrere `input()`-Funktionen gibt, werden die Werte in der Reihenfolge bereitgestellt, in der sie im `input`-Tag geschrieben sind.
-Wenn Sie `input()` öfter aufrufen als die Anzahl der bereitgestellten Werte, wird ein Fehler ausgelöst.
+Wenn Ihr Code `input()` aufruft, zeigt der Browser einen Prompt-Dialog für die Eingabe an.
````md
:::pyide
-```input
-a
-b
-c
-d
-```
-
```python
-print(input())
-print(input())
-print(input())
-print(input())
+a = input("Enter a value: ")
+print(a)
```
:::
@@ -142,18 +143,10 @@ print(input())
:::pyide
-```input
-a
-b
-c
-d
-```
```python
-print(input())
-print(input())
-print(input())
-print(input())
+a = input("Enter a value: ")
+print(a)
```
:::
@@ -161,9 +154,70 @@ print(input())
## Ausführung stoppen
:::alert{warn}
-Das Stoppen einer Endlosschleife oder eines lang andauernden Prozesses ist nur durch Aktualisieren der Seite oder durch Setzen dieser beiden Header auf Ihrem Server möglich:
+Nutzen Sie den **Stoppen**-Button im Editor, um eine Unterbrechung anzufordern.
+Das Stoppen einer Endlosschleife oder eines lang andauernden Prozesses ist jedoch nur zuverlässig, wenn diese beiden Header auf Ihrem Server gesetzt sind:
```
'Cross-Origin-Embedder-Policy': 'require-corp'
'Cross-Origin-Opener-Policy': 'same-origin'
```
-:::
\ No newline at end of file
+:::
+
+## Pygame
+
+:::pyide{canvas}
+
+```python
+import pygame
+import asyncio
+
+async def run_game():
+ fps = 60
+ pygame.init()
+ screen = pygame.display.set_mode((400, 300))
+ r = 0
+ g = 0
+ b = 0
+
+ while True:
+ for event in pygame.event.get():
+ if event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_r:
+ r = (r + 50) % 256
+ elif event.key == pygame.K_g:
+ g = (g + 50) % 256
+ elif event.key == pygame.K_b:
+ b = (b + 50) % 256
+ elif event.type == pygame.K_ESCAPE or event.type == pygame.QUIT:
+ return
+ screen.fill((r, g, b))
+ pygame.display.flip()
+ await asyncio.sleep(1 / fps)
+
+asyncio.run(run_game())
+```
+
+:::
+
+## Pytamaro
+
+:::pyide{packages="pytamaro"}
+
+```python
+from pytamaro import *
+
+block_size = 25
+num_blocks = 16
+line = empty_graphic()
+for col in range(num_blocks):
+ if col % 2 == 0:
+ color = black
+ else:
+ color = white
+ block = rectangle(block_size, block_size, color)
+ line = beside(line, block)
+second_line = rotate(180, line)
+finish_line = above(line, second_line)
+show_graphic(finish_line)
+```
+
+:::
diff --git a/website/en/book/elements/pyide.md b/website/en/book/elements/pyide.md
index e8dbde2e4..cdc8cc752 100644
--- a/website/en/book/elements/pyide.md
+++ b/website/en/book/elements/pyide.md
@@ -3,6 +3,8 @@ name: Pyide
permaid: pyide
---
+# Pyide
+
The `pyide` element represents a Python Integrated Development Environment (IDE) component.
It is used to embed a Python coding environment within the hyperbook website.
This element allows users to write, edit, and execute Python code directly in the browser.
@@ -32,6 +34,20 @@ print(a)
You can also use any package listed here: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
+If you need packages from PyPI, use the `packages` attribute with a comma-separated list. Hyperbook loads `micropip` and installs these packages before executing your script.
+
+````md
+:::pyide{packages="snowballstemmer"}
+
+```python
+import snowballstemmer
+stemmer = snowballstemmer.stemmer("english")
+print(stemmer.stemWords(["running", "runner", "runs"]))
+```
+
+:::
+````
+
````md
:::pyide
@@ -119,27 +135,14 @@ def check_palindrom(s):
:::
-## Input()
-
-You can use the `input()` function in the code snippets. The `input()` function is replaced by the values provided in the `input` tag.
-If there are multiple `input()` functions, the values are provided in the order they are written in the `input` tag.
-If you call `input()` more times than the number of values provided, the code will throw an error.
+When your code calls `input()`, the browser shows a prompt dialog for the value.
````md
:::pyide
-```input
-a
-b
-c
-d
-```
-
```python
-print(input())
-print(input())
-print(input())
-print(input())
+a = input("Enter a value: ")
+print(a)
```
:::
@@ -147,18 +150,10 @@ print(input())
:::pyide
-```input
-a
-b
-c
-d
-```
```python
-print(input())
-print(input())
-print(input())
-print(input())
+a = input("Enter a value: ")
+print(a)
```
:::
@@ -166,9 +161,70 @@ print(input())
## Stopping the execution
:::alert{warn}
-Stopping an infinite loop or a long lasting process is only possible by refreshing the page or by setting these two headers on your server:
+Use the **Stop** button in the editor to request an interrupt.
+For infinite loops or long-running processes, interruption is only reliable when these two headers are set on your server:
```
'Cross-Origin-Embedder-Policy': 'require-corp'
'Cross-Origin-Opener-Policy': 'same-origin'
```
-:::
\ No newline at end of file
+:::
+
+## Pygame
+
+:::pyide{canvas}
+
+```python
+import pygame
+import asyncio
+
+async def run_game():
+ fps = 60
+ pygame.init()
+ screen = pygame.display.set_mode((400, 300))
+ r = 0
+ g = 0
+ b = 0
+
+ while True:
+ for event in pygame.event.get():
+ if event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_r:
+ r = (r + 50) % 256
+ elif event.key == pygame.K_g:
+ g = (g + 50) % 256
+ elif event.key == pygame.K_b:
+ b = (b + 50) % 256
+ elif event.type == pygame.K_ESCAPE or event.type == pygame.QUIT:
+ return
+ screen.fill((r, g, b))
+ pygame.display.flip()
+ await asyncio.sleep(1 / fps)
+
+asyncio.run(run_game())
+```
+
+:::
+
+## Pytamaro
+
+:::pyide{packages="pytamaro"}
+
+```python
+from pytamaro import *
+
+block_size = 25
+num_blocks = 16
+line = empty_graphic()
+for col in range(num_blocks):
+ if col % 2 == 0:
+ color = black
+ else:
+ color = white
+ block = rectangle(block_size, block_size, color)
+ line = beside(line, block)
+second_line = rotate(180, line)
+finish_line = above(line, second_line)
+show_graphic(finish_line)
+```
+
+:::