From 2fb4a3876bf7d098b67c9fd0585b8d5cf036127d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remko=20Tron=C3=A7on?= Date: Sun, 20 Nov 2022 13:02:13 +0100 Subject: [PATCH] notebook: Updates --- .github/workflows/publish-notebook.yml | 25 +++ package.json | 3 +- src/web/notebook/.eslintrc.yml | 7 + src/web/notebook/build.js | 15 +- .../examples/drawing-with-forth.wafnb | 8 +- src/web/notebook/src/generator.ts | 18 +- src/web/notebook/src/wafnb.css | 110 +++++++++++- src/web/notebook/src/wafnb.ts | 12 -- src/web/notebook/src/wafnb.tsx | 157 ++++++++++++++++++ src/web/thurtle/Editor.tsx | 17 +- src/web/thurtle/draw.tsx | 9 +- src/web/vscode-extension/src/extension.ts | 8 +- 12 files changed, 355 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/publish-notebook.yml create mode 100644 src/web/notebook/.eslintrc.yml delete mode 100644 src/web/notebook/src/wafnb.ts create mode 100644 src/web/notebook/src/wafnb.tsx diff --git a/.github/workflows/publish-notebook.yml b/.github/workflows/publish-notebook.yml new file mode 100644 index 0000000..6cc6e00 --- /dev/null +++ b/.github/workflows/publish-notebook.yml @@ -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 diff --git a/package.json b/package.json index 98c851c..5d765db 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/web/notebook/.eslintrc.yml b/src/web/notebook/.eslintrc.yml new file mode 100644 index 0000000..dfb916a --- /dev/null +++ b/src/web/notebook/.eslintrc.yml @@ -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 diff --git a/src/web/notebook/build.js b/src/web/notebook/build.js index 9d0a456..55012b1 100755 --- a/src/web/notebook/build.js +++ b/src/web/notebook/build.js @@ -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) { diff --git a/src/web/notebook/examples/drawing-with-forth.wafnb b/src/web/notebook/examples/drawing-with-forth.wafnb index f43f7c9..dbde916 100644 --- a/src/web/notebook/examples/drawing-with-forth.wafnb +++ b/src/web/notebook/examples/drawing-with-forth.wafnb @@ -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, diff --git a/src/web/notebook/src/generator.ts b/src/web/notebook/src/generator.ts index 773af2b..73f419e 100644 --- a/src/web/notebook/src/generator.ts +++ b/src/web/notebook/src/generator.ts @@ -50,7 +50,11 @@ export async function generate({ }) ); let title: string | null = null; - let out = ["
"]; + let out: string[] = []; + out.push( + "" + ); + out.push("
"); for (const cell of nb.cells) { switch (cell.kind) { case 1: @@ -83,6 +87,16 @@ export async function generate({ } out.push("
"); - out = [`${escapeHTML(title ?? "")}`, style, ...out, script]; + out = [ + `${escapeHTML( + title ?? "" + )}`, + style, + "", + "", + ...out, + script, + "", + ]; return fs.promises.writeFile(outfile, out.join("\n")); } diff --git a/src/web/notebook/src/wafnb.css b/src/web/notebook/src/wafnb.css index 51c59cd..dd6c004 100644 --- a/src/web/notebook/src/wafnb.css +++ b/src/web/notebook/src/wafnb.css @@ -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; } diff --git a/src/web/notebook/src/wafnb.ts b/src/web/notebook/src/wafnb.ts deleted file mode 100644 index b3402a4..0000000 --- a/src/web/notebook/src/wafnb.ts +++ /dev/null @@ -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 -} diff --git a/src/web/notebook/src/wafnb.tsx b/src/web/notebook/src/wafnb.tsx new file mode 100644 index 0000000..54d6063 --- /dev/null +++ b/src/web/notebook/src/wafnb.tsx @@ -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 = () => ( + + + +); + +const clearIcon = () => ( + + + +); + +const runs: Array<() => Promise> = []; +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 =
; + 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 = ; + const consoleEl: HTMLPreElement =
;
+      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 = (
+    
+  );
+  const clearEl = (
+    
+  );
+  clearEl.style.display = "none";
+  n.insertBefore(
+    
+ {runEl} + {clearEl} +
, + 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 = ( + +); +const clearAllButtonEl = ( + +); +const controlsEl = ( +
+ {runAllButtonEl} + {clearAllButtonEl} +
+); +contentEl.insertBefore(controlsEl, contentEl.firstElementChild); diff --git a/src/web/thurtle/Editor.tsx b/src/web/thurtle/Editor.tsx index f83a6db..5e367c7 100644 --- a/src/web/thurtle/Editor.tsx +++ b/src/web/thurtle/Editor.tsx @@ -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 = ; this.codeEl = ; this.preEl = ; @@ -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; } diff --git a/src/web/thurtle/draw.tsx b/src/web/thurtle/draw.tsx index 2626dc7..dc07b7f 100644 --- a/src/web/thurtle/draw.tsx +++ b/src/web/thurtle/draw.tsx @@ -26,13 +26,14 @@ export default async function draw({ onEmit?: (c: string) => void; showTurtle?: boolean; jsx: any; -}): Promise { +}) { // 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 = [{ 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 }; } diff --git a/src/web/vscode-extension/src/extension.ts b/src/web/vscode-extension/src/extension.ts index ea5e3ad..9c81390 100644 --- a/src/web/vscode-extension/src/extension.ts +++ b/src/web/vscode-extension/src/extension.ts @@ -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;";