Skip to content
Merged

Pyide #923

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/blue-ways-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"hyperbook": minor
"@hyperbook/markdown": minor
---

Add pyide
6 changes: 5 additions & 1 deletion packages/hyperbook/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ async function runBuild(
case "archive":
return posix.join("/", basePath || "", "archives", ...path);
case "assets":
return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}?version=${packageJson.version}`;
if (path.length === 1 && path[0] === "/") {
return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}`;
} else {
return `${posix.join("/", basePath || "", ASSETS_FOLDER, ...path)}?version=${packageJson.version}`;
}
}
},
project: rootProject,
Expand Down
89 changes: 89 additions & 0 deletions packages/markdown/assets/directive-pyide/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
hyperbook.python = (function () {
window.codeInput?.registerTemplate(
"pyide-highlighted",
codeInput.templates.prism(window.Prism, [
new codeInput.plugins.AutoCloseBrackets(),
new codeInput.plugins.Indent(true, 2),
])
);

const pyodideWorker = new Worker(
`${HYPERBOOK_ASSETS}directive-pyide/webworker.js`
);

const callbacks = {};
let isRunning = false;

const asyncRun = (id) => {
if (isRunning) return;

isRunning = true;
updateRunning();
return (script, context) => {
// the id could be generated more carefully
return new Promise((onSuccess) => {
callbacks[id] = onSuccess;
pyodideWorker.postMessage({
...context,
python: script,
id,
});
});
};
};

const updateRunning = () => {
for (let elem of elems) {
const run = elem.getElementsByClassName("run")[0];
if (isRunning) {
run.classList.add("running");
run.textContent = "Running ...";
} else {
run.classList.remove("running");
run.textContent = "Run";
}
run.disabled = isRunning;
}
};

pyodideWorker.onmessage = (event) => {
const { id, ...data } = event.data;
if (data.type === "stdout") {
const output = document
.getElementById(id)
.getElementsByClassName("output")[0];
output.appendChild(document.createTextNode(data.message + "\n"));
return;
}
const onSuccess = callbacks[id];
delete callbacks[id];
isRunning = false;
updateRunning();
onSuccess(data);
};

const elems = document.getElementsByClassName("directive-pyide");

for (let elem of elems) {
const editor = elem.getElementsByClassName("editor")[0];
const run = elem.getElementsByClassName("run")[0];
const output = elem.getElementsByClassName("output")[0];
const id = elem.id;

run?.addEventListener("click", () => {
const script = editor.value;
output.innerHTML = "";
asyncRun(id)(script, {})
.then(({ results, error }) => {
if (results) {
output.textContent = results;
} else if (error) {
output.textContent = error;
}
})
.catch((e) => {
output.textContent = `Error: ${e}`;
});
});
}
})();
86 changes: 86 additions & 0 deletions packages/markdown/assets/directive-pyide/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.directive-pyide {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 16px;
overflow: hidden;
gap: 8px;
}

.directive-pyide code-input {
margin: 0;
}

.directive-pyide .container {
width: 100%;
border: 1px solid var(--color-spacer);
border-radius: 8px;
overflow: hidden;
padding: 10px;
}

.directive-pyide .output {
height: 200px;
white-space: pre;
}

.directive-pyide .editor-container {
width: 100%;
display: flex;
flex-direction: column;
height: 400px;
}

.directive-pyide .editor {
width: 100%;
border: 1px solid var(--color-spacer);
border-radius: 8px;
border-top-left-radius: 0;
border-top-right-radius: 0;
flex: 1;
}

.directive-pyide button {
padding: 8px 16px;
border: 1px solid var(--color-spacer);
border-radius: 8px;
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: var(--color--background);
color: var(--color-text);
cursor: pointer;
}

.directive-pyide button:hover {
background-color: var(--color-spacer);
}

.directive-pyide button.running {
pointer-events: none;
cursor: not-allowed;
opacity: 0.5;
}

@media screen and (min-width: 1024px) {
.directive-pyide {
flex-direction: row;
height: calc(100dvh - 128px);

.output {
height: 100%;
}

.container {
flex: 1;
height: 100% !important;
}

.editor-container {
flex: 3;
height: 100%;
overflow: hidden;
}
}
}
42 changes: 42 additions & 0 deletions packages/markdown/assets/directive-pyide/webworker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Setup your project to serve `py-worker.js`. You should also serve
// `pyodide.js`, and all its associated `.asm.js`, `.json`,
// and `.wasm` files as well:
importScripts("https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js");

async function loadPyodideAndPackages() {
self.pyodide = await loadPyodide();
await self.pyodide.loadPackage([]);
}
let pyodideReadyPromise = loadPyodideAndPackages();

self.onmessage = async (event) => {
// make sure loading is done
await pyodideReadyPromise;
// Don't bother yet with this line, suppose our API is built in such a way:
const { id, python, ...context } = event.data;
// The worker copies the context in its own "memory" (an object mapping name to values)
for (const key of Object.keys(context)) {
self[key] = context[key];
}

self.pyodide.setStdout({
batched: (msg) => {
self.postMessage({ id, type: "stdout", message: msg });
},
});

self.pyodide.setStderr({
batched: (msg) => {
self.postMessage({ id, type: "stderr", message: msg });
},
});

// Now is the easy part, the one that is similar to working in the main thread:
try {
await self.pyodide.loadPackagesFromImports(python);
let results = await self.pyodide.runPythonAsync(python);
self.postMessage({ results, id });
} catch (error) {
self.postMessage({ error: error.message, id });
}
};
3 changes: 2 additions & 1 deletion packages/markdown/assets/prism/prism.js

Large diffs are not rendered by default.

146 changes: 11 additions & 135 deletions packages/markdown/dev.md
Original file line number Diff line number Diff line change
@@ -1,145 +1,21 @@
# Test Site

:::p5{height=400 editor=true}
:::pyide

```js
let circleX = 200;
let circleY = 150;
let circleRadius = 75;

let graphX = 50;
let graphY = 300;
let graphAmplitude = 50;
let graphPeriod = 300;

function setup() {
createCanvas(400, 400);
angleMode(DEGREES);
describe(
'Animated demonstration of a point moving around the unit circle, together with the corresponding sine and cosine values moving along their graphs.'
);
}

function draw() {
background(0);

// Set angle based on frameCount, and display current value

let angle = frameCount % 360;

fill(255);
textSize(20);
textAlign(LEFT, CENTER);
text(`angle: ${angle}`, 25, 25);

// Draw circle and diameters

noFill();
stroke(128);
strokeWeight(3);
circle(circleX, circleY, 2 * circleRadius);
line(circleX, circleY - circleRadius, circleX, circleY + circleRadius);
line(circleX - circleRadius, circleY, circleX + circleRadius, circleY);

// Draw moving points

let pointX = circleX + circleRadius * cos(angle);
let pointY = circleY - circleRadius * sin(angle);

line(circleX, circleY, pointX, pointY);

noStroke();

fill('white');
circle(pointX, pointY, 10);

fill('orange');
circle(pointX, circleY, 10);

fill('red');
circle(circleX, pointY, 10);

// Draw graph

stroke('grey');
strokeWeight(3);
line(graphX, graphY, graphX + 300, graphY);
line(graphX, graphY - graphAmplitude, graphX, graphY + graphAmplitude);
line(
graphX + graphPeriod,
graphY - graphAmplitude,
graphX + graphPeriod,
graphY + graphAmplitude
);

fill('grey');
strokeWeight(1);
textAlign(CENTER, CENTER);
text('0', graphX, graphY + graphAmplitude + 20);
text('360', graphX + graphPeriod, graphY + graphAmplitude + 20);
text('1', graphX / 2, graphY - graphAmplitude);
text('0', graphX / 2, graphY);
text('-1', graphX / 2, graphY + graphAmplitude);

fill('orange');
text('cos', graphX + graphPeriod + graphX / 2, graphY - graphAmplitude);
fill('red');
text('sin', graphX + graphPeriod + graphX / 2, graphY);

// Draw cosine curve

noFill();
stroke('orange');
beginShape();
for (let t = 0; t <= 360; t++) {
let x = map(t, 0, 360, graphX, graphX + graphPeriod);
let y = graphY - graphAmplitude * cos(t);
vertex(x, y);
}
endShape();

// Draw sine curve

noFill();
stroke('red');
beginShape();
for (let t = 0; t <= 360; t++) {
let x = map(t, 0, 360, graphX, graphX + graphPeriod);
let y = graphY - graphAmplitude * sin(t);
vertex(x, y);
}
endShape();

// Draw moving line

let lineX = map(angle, 0, 360, graphX, graphX + graphPeriod);
stroke('grey');
line(lineX, graphY - graphAmplitude, lineX, graphY + graphAmplitude);

// Draw moving points on graph
```python
a = 5 + 2
print(a)
```

let orangeY = graphY - graphAmplitude * cos(angle);
let redY = graphY - graphAmplitude * sin(angle);
:::

noStroke();
:::pyide

fill('orange');
circle(lineX, orangeY, 10);

fill('red');
circle(lineX, redY, 10);
}
```python
a = 5 + 2
print(a)
```

:::

```abcjs editor
X:1
T: Cooley's Long
M: 4/4
L: 1/8\nR: reel\nK: Emin
D2|:"Em"EB{c}BA B2 EB|~B2 AB dBAG|"D"FDAD BDAD|FDAD dAFD|
"Em"EBBA B2 EB|B2 AB defg|"D"afe^c dBAF|1"Em"DEFD E2 D2:|2"Em"DEFD E2 gf||
|:"Em"eB B2 efge|eB B2 gedB|"D"A2 FA DAFA|A2 FA defg|
"Em"eB B2 eBgB|eB B2 defg|"D"afe^c dBAF|1"Em"DEFD E2 gf:|2"Em"DEFD E4|]
```
:::
Loading
Loading