mirror of
https://github.com/remko/waforth
synced 2025-01-13 08:01:32 +01:00
notebook: Updates
This commit is contained in:
parent
a29e0f9567
commit
2fb4a3876b
12 changed files with 355 additions and 34 deletions
25
.github/workflows/publish-notebook.yml
vendored
Normal file
25
.github/workflows/publish-notebook.yml
vendored
Normal 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
|
|
@ -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",
|
||||
|
|
7
src/web/notebook/.eslintrc.yml
Normal file
7
src/web/notebook/.eslintrc.yml
Normal 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
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
157
src/web/notebook/src/wafnb.tsx
Normal file
157
src/web/notebook/src/wafnb.tsx
Normal 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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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;";
|
||||
|
|
Loading…
Reference in a new issue