notebook: Updates

This commit is contained in:
Remko Tronçon 2022-11-20 13:02:13 +01:00
parent a29e0f9567
commit 2fb4a3876b
12 changed files with 355 additions and 34 deletions

25
.github/workflows/publish-notebook.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Publish Notebook
on:
workflow_dispatch:
jobs:
build:
uses: ./.github/workflows/build.yml
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/setup
- run: sudo apt-get update
- run: sudo apt-get install awscli
- run: make -C src/web/notebook
- run: ./dist/wafnb2html src/web/notebook/examples/drawing-with-forth.wafnb
- name: aws s3 sync
run: |
aws configure set region eu-central-1
aws configure set aws_access_key_id ${{secrets.AWS_ACCESS_KEY_ID}}
aws configure set aws_secret_access_key ${{secrets.AWS_SECRET_ACCESS_KEY}}
aws s3 sync src/web/notebook/examples/drawing-with-forth.html s3://${{secrets.AWS_SITE_BUCKET}}/wafnb/drawing-with-forth/index.html

View file

@ -32,6 +32,7 @@
},
"types": "dist/waforth.d.ts",
"main": "dist/index.js",
"bin": "./src/web/notebook/dist/wafnb2html",
"files": [
"dist"
],
@ -41,7 +42,7 @@
"test": "node test-web.js",
"test-watch": "node test-web.js --watch",
"lint": "eslint . && tsc --noEmit",
"prepare": "node build-package.js"
"prepare": "node build-package.js && cd src/web/notebook && node build.js"
},
"keywords": [
"forth",

View file

@ -0,0 +1,7 @@
rules:
# Due to our custom JSX. There are probably better rules.
'react/react-in-jsx-scope': 0
'@typescript-eslint/no-unused-vars': 0
'react/no-unknown-property': 0
'react/no-unescaped-entities': 0
'@typescript-eslint/no-namespace': 0

View file

@ -35,7 +35,7 @@ const buildConfig = {
let nbBuildConfig = {
...buildConfig,
outdir: path.join(__dirname, "dist"),
entryPoints: [path.join(__dirname, "src", "wafnb.ts")],
entryPoints: [path.join(__dirname, "src", "wafnb.tsx")],
publicPath: "/dist",
assetNames: "[name].txt",
sourcemap: !!dev,
@ -45,11 +45,20 @@ let nbBuildConfig = {
},
};
const generatorOutFile = path.join(
__dirname,
"..",
"..",
"..",
"dist",
"wafnb2html"
);
let generatorBuildConfig = {
...buildConfig,
banner: { js: "#!/usr/bin/env node" },
platform: "node",
outfile: path.join(__dirname, "dist", "wafnb2html"),
outfile: generatorOutFile,
entryPoints: [path.join(__dirname, "src", "wafnb2html.mjs")],
sourcemap: dev ? "inline" : undefined,
loader: {
@ -66,7 +75,7 @@ let generatorBuildConfig = {
};
function handleGeneratorBuildFinished(result) {
return fs.chmodSync(path.join(__dirname, "dist", "wafnb2html"), "755");
return fs.chmodSync(generatorOutFile, "755");
}
if (watch) {

View file

@ -3,7 +3,7 @@
{
"kind": 1,
"language": "markdown",
"value": "# Drawing with Forth\n\nIn this tutorial, you'll learn basic Forth by drawing graphics with a turtle.\n\n| *VS Code Note:* Make sure you select the *Thurtle* kernel in the top right above to run this notebook\n\n## The stack\n\nForth is a stack-based language. Numbers are put on the stack, and words pop them off the stack (and put new ones on the stack) again.\nFor example, to take the sum of 8 and 14, put both numbers on the stack, and call `+`. To pop the result of the stack and print it out, use `.`:"
"value": "# Drawing with Forth\n\nIn this tutorial, you'll learn basic Forth by drawing graphics with a turtle.\n\n> 💡 Click on the *Run* button next to the examples to run the code.\n\n## The stack\n\nForth is a stack-based language. Numbers are put on the stack, and words pop them off the stack (and put new ones on the stack) again.\nFor example, to take the sum of 8 and 14, put both numbers on the stack, and call `+`. To pop the result of the stack and print it out, use `.`:"
},
{
"kind": 2,
@ -53,7 +53,7 @@
{
"kind": 1,
"language": "markdown",
"value": "## Combining words\n\nWe can create more complex figures by using the `SQUARE` word from above, and repeating it.\n\n| *Tip*: Play with the numbers in `FLOWER` to create variations of the flower "
"value": "## Combining words\n\nWe can create more complex figures by using the `SQUARE` word from above, and repeating it.\n\n> 💡 Play with the numbers in `FLOWER` to create variations of the flower "
},
{
"kind": 2,
@ -73,7 +73,7 @@
{
"kind": 1,
"language": "markdown",
"value": "We can make a small variation of the above recursive program, where the lines become longer instead of shorter.\nTo avoid hard-coding some constants in the code, we use the word `CONSTANT` to define a new constant (`ANGLE`) to turn.\n\n| *Tip*: Change the constant `ANGLE` to 91 and see what happens."
"value": "We can make a small variation of the above recursive program, where the lines become longer instead of shorter.\nTo avoid hard-coding some constants in the code, we use the word `CONSTANT` to define a new constant (`ANGLE`) to turn.\n\n> 💡 Change the constant `ANGLE` to 91 and see what happens."
},
{
"kind": 2,
@ -83,7 +83,7 @@
{
"kind": 1,
"language": "markdown",
"value": "## Fractals\n\nYou can create more complex recursive drawings, called *fractals*. \n\nA famous fractal is the *Koch snowflake*.\n\n| *Tip*: Change the `DEPTH` constant to make a coarser or finer grained snowflake"
"value": "## Fractals\n\nYou can create more complex recursive drawings, called *fractals*. \n\nA famous fractal is the *Koch snowflake*.\n\n> 💡 Change the `DEPTH` constant to make a coarser or finer grained snowflake"
},
{
"kind": 2,

View file

@ -50,7 +50,11 @@ export async function generate({
})
);
let title: string | null = null;
let out = ["<div class='content'>"];
let out: string[] = [];
out.push(
"<div class='banner'>Powered by <a target='_blank' rel='noreferrer' href='https://github.com/remko/waforth'>WAForth</a></div>"
);
out.push("<div class='content' data-hook='content'>");
for (const cell of nb.cells) {
switch (cell.kind) {
case 1:
@ -83,6 +87,16 @@ export async function generate({
}
out.push("</div>");
out = [`<title>${escapeHTML(title ?? "")}</title>`, style, ...out, script];
out = [
`<html><head><meta charset="utf-8"><title>${escapeHTML(
title ?? ""
)}</title>`,
style,
"</head>",
"<body>",
...out,
script,
"</body></html>",
];
return fs.promises.writeFile(outfile, out.join("\n"));
}

View file

@ -1,11 +1,49 @@
*,
*:before,
*:after {
box-sizing: border-box;
}
body {
font-family: sans-serif;
color: rgb(33, 37, 41);
}
.text-cell code {
color: #d63384;
}
blockquote {
border-radius: 0.5em;
padding: 0.5em 0.5em;
background-color: rgb(225, 242, 254);
color: rgb(23, 70, 127);
}
blockquote p {
margin: 0;
}
.banner {
color: #aaa;
text-align: center;
font-size: 0.8em;
margin-top: 1em;
}
.banner a {
color: #aaa;
}
.banner a:hover {
color: #aaa;
}
.content {
max-width: 40em;
max-width: 45em;
margin-left: auto;
margin-right: auto;
padding: 0em 22px;
}
code.raw-code-cell {
@ -16,6 +54,72 @@ code.raw-code-cell {
background-color: #eee;
}
.editor {
flex: 1;
.code-cell {
display: flex;
gap: 0.25em;
}
.output {
gap: 0.125em;
display: flex;
flex-direction: column;
}
.console {
font-family: monospace;
background-color: #111;
color: white;
padding: 0.5em 1em;
border-radius: 0.25em;
margin: 0;
}
.code-cell .editor {
flex: 1;
background-color: #eee;
box-sizing: content-box;
}
.world {
background-color: rgb(221, 248, 221);
border-radius: 10px;
width: 100%;
max-height: 15em;
}
.all-controls {
float: right;
display: flex;
gap: 4px;
margin-top: 4px;
}
.controls {
float: left;
margin-left: -28px;
display: flex;
flex-direction: column;
}
.bi {
display: inline-block;
vertical-align: -0.125em;
fill: currentcolor;
}
.toolbutton {
display: inline-block;
border: none;
border-radius: 0.5em;
background: none;
color: #888;
height: 24px;
width: 24px;
padding: 0;
cursor: pointer;
}
.toolbutton:hover {
background-color: rgba(0, 0, 0, 0.1);
color: inherit;
}

View file

@ -1,12 +0,0 @@
import "./wafnb.css";
import Editor from "../../thurtle/Editor";
for (const n of document.querySelectorAll("[data-hook=code-cell")) {
n.className = "code-cell";
const program = n.textContent ?? "";
const editor = new Editor();
editor.setValue(program);
n.innerHTML = "";
n.appendChild(editor.el);
editor.el.style.minHeight = "10em"; // FIXME
}

View file

@ -0,0 +1,157 @@
import "./wafnb.css";
import Editor from "../../thurtle/Editor";
import * as jsx from "../../thurtle/jsx";
import draw from "../../thurtle/draw";
const runIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"
/>
</svg>
);
const clearIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828l6.879-6.879zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414l-3.879-3.879zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293l.16-.16z"
/>
</svg>
);
const runs: Array<() => Promise<void>> = [];
const clears: Array<() => void> = [];
const setEnableds: Array<(v: boolean) => void> = [];
for (const n of document.querySelectorAll("[data-hook=code-cell")) {
n.className = "code-cell";
const program = n.textContent ?? "";
const editor = new Editor(true);
editor.setValue(program);
n.innerHTML = "";
n.appendChild(editor.el);
const outputEl = <div class="output" />;
n.appendChild(outputEl);
const setEnabled = (v: boolean) => {
runEl.disabled = !v;
};
setEnableds.push(setEnabled);
const clear = () => {
outputEl.innerHTML = "";
clearEl.style.display = "none";
};
clears.push(clear);
const run = async () => {
setEnabled(false);
try {
clear();
const worldEl = <svg class="world" xmlns="http://www.w3.org/2000/svg" />;
const consoleEl: HTMLPreElement = <pre class="console" />;
const result = await draw({
program: editor.getValue(),
drawEl: worldEl,
onEmit: (c: string) => {
// TODO: Buffer lines
consoleEl.appendChild(document.createTextNode(c));
},
jsx,
});
if (!result.isEmpty) {
outputEl.appendChild(worldEl);
}
if (consoleEl.childNodes.length > 0) {
outputEl.appendChild(consoleEl);
}
clearEl.style.display = "block";
} catch (e) {
alert((e as any).message);
} finally {
setEnabled(true);
}
};
runs.push(run);
const runEl = (
<button title="Run" class="toolbutton" onclick={run}>
{runIcon()}
</button>
);
const clearEl = (
<button title="Clear" class="toolbutton" onclick={clear}>
{clearIcon()}
</button>
);
clearEl.style.display = "none";
n.insertBefore(
<div class="controls">
{runEl}
{clearEl}
</div>,
editor.el
);
}
function setAllEnabled(v: boolean) {
for (const setEnabled of setEnableds) {
setEnabled(v);
}
runAllButtonEl.disabled = !v;
clearAllButtonEl.disabled = !v;
}
async function runAll() {
setAllEnabled(false);
try {
for (const run of runs) {
await run();
}
} catch (e) {
// do nothing
} finally {
setAllEnabled(true);
}
}
async function clearAll() {
for (const clear of clears) {
clear();
}
}
const contentEl = document.querySelector("[data-hook=content]")!;
const runAllButtonEl = (
<button title="Run all" class="toolbutton" onclick={runAll}>
{runIcon()}
</button>
);
const clearAllButtonEl = (
<button title="Clear all" class="toolbutton" onclick={clearAll}>
{clearIcon()}
</button>
);
const controlsEl = (
<div class="all-controls">
{runAllButtonEl}
{clearAllButtonEl}
</div>
);
contentEl.insertBefore(controlsEl, contentEl.firstElementChild);

View file

@ -26,8 +26,10 @@ export default class Editor {
codeEl: HTMLElement;
preEl: HTMLPreElement;
el: HTMLDivElement;
autoResize: boolean;
constructor() {
constructor(autoResize = false) {
this.autoResize = autoResize;
this.textEl = <textarea autofocus spellcheck={false}></textarea>;
this.codeEl = <code class="language-forth" />;
this.preEl = <pre aria-hidden="true">{this.codeEl}</pre>;
@ -39,16 +41,22 @@ export default class Editor {
);
this.textEl.addEventListener("input", (ev) => {
this.#setCode(this.textEl.value);
this.#updateHeight();
this.#updateScroll();
});
this.textEl.addEventListener("scroll", () => {
this.#updateScroll();
});
this.#updateHeight();
}
setValue(v: string) {
this.textEl.value = v;
this.#setCode(v);
if (this.autoResize) {
// Hack, because the scrollHeight isn't set yet. Not sure what the proper solution is
window.setTimeout(() => this.#updateHeight(), 0);
}
}
#setCode(v: string) {
@ -97,6 +105,13 @@ export default class Editor {
this.preEl.scrollLeft = this.textEl.scrollLeft;
}
#updateHeight() {
if (this.autoResize) {
this.el.style.height = "0";
this.el.style.height = this.textEl.scrollHeight + "px";
}
}
getValue(): string {
return this.textEl.value;
}

View file

@ -26,13 +26,14 @@ export default async function draw({
onEmit?: (c: string) => void;
showTurtle?: boolean;
jsx: any;
}): Promise<ErrorCode | null> {
}) {
// Initialize state
let rotation = 270;
const position = { x: 0, y: 0 };
const boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
let pen = PenState.Down;
let visible = true;
let isEmpty = true;
const paths: Array<Path> = [{ d: [`M${position.x} ${position.y}`] }];
function updatePosition(x: number, y: number) {
@ -53,7 +54,7 @@ export default async function draw({
}
// Run program
let result: ErrorCode | null = null;
let result = ErrorCode.Quit;
if (program != null) {
const forth = new WAForth();
await forth.load();
@ -65,6 +66,7 @@ export default async function draw({
paths[paths.length - 1].d.push(
[pen === PenState.Down ? "l" : "m", dx, dy].join(" ")
);
isEmpty = isEmpty && pen !== PenState.Down;
updatePosition(position.x + dx, position.y + dy);
});
@ -91,6 +93,7 @@ export default async function draw({
paths[paths.length - 1].d.push(
[pen === PenState.Down ? "l" : "M", x, y].join(" ")
);
isEmpty = isEmpty && pen !== PenState.Down;
updatePosition(x, y);
});
@ -181,5 +184,5 @@ export default async function draw({
drawEl.appendChild(turtleEl);
}
return result;
return { isEmpty, result };
}

View file

@ -150,17 +150,15 @@ async function createNotebookController(
const svgEl = jsx.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg",
});
result = (await draw({
const drawResult = (await draw({
program,
drawEl: svgEl as any,
onEmit: emit,
showTurtle: true,
jsx,
}))!;
const paths = (svgEl._children as any[]).find(
(el) => el._tag === "g"
)._children;
if (paths.length > 1 || paths[0].d !== "M0 0") {
result = drawResult.result;
if (!drawResult.isEmpty) {
svgEl.height = "300px";
svgEl.style =
"background-color: rgb(221, 248, 221); border-radius: 10px;";