From a29e0f9567f1970d39769f9c18352a3530d2afef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remko=20Tron=C3=A7on?= Date: Sun, 20 Nov 2022 09:20:35 +0100 Subject: [PATCH] notebook: Move some files --- .github/workflows/build.yml | 1 + README.md | 4 +- package.json | 2 + scripts/esbuild/watcher.js | 12 +- src/web/notebook/.gitignore | 2 + src/web/notebook/Makefile | 8 ++ src/web/notebook/build.js | 106 ++++++++++++++++++ .../examples/drawing-with-forth.wafnb | 9 +- src/web/notebook/src/Notebook.ts | 21 ++++ src/web/notebook/src/generator.ts | 88 +++++++++++++++ src/web/notebook/src/wafnb.css | 21 ++++ src/web/notebook/src/wafnb.ts | 12 ++ src/web/notebook/src/wafnb2html.mjs | 13 +++ src/web/vscode-extension/README.md | 8 +- src/web/vscode-extension/package.json | 2 +- src/web/vscode-extension/src/extension.ts | 30 ++--- yarn.lock | 10 ++ 17 files changed, 307 insertions(+), 42 deletions(-) create mode 100644 src/web/notebook/.gitignore create mode 100644 src/web/notebook/Makefile create mode 100755 src/web/notebook/build.js rename src/web/{vscode-extension => notebook}/examples/drawing-with-forth.wafnb (86%) create mode 100644 src/web/notebook/src/Notebook.ts create mode 100644 src/web/notebook/src/generator.ts create mode 100644 src/web/notebook/src/wafnb.css create mode 100644 src/web/notebook/src/wafnb.ts create mode 100644 src/web/notebook/src/wafnb2html.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 279787a..a5ee679 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,5 +13,6 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/setup - run: yarnpkg build + - run: make -C src/web/notebook - run: yarnpkg lint - run: yarnpkg test --coverage diff --git a/README.md b/README.md index d106b83..049a1c6 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,9 @@ Because it is powered by WebAssembly, this extension works both in the desktop v
-WAForth notebook +WAForth notebook
-
WAForth notebook
+
WAForth notebook
diff --git a/package.json b/package.json index 6b9f4fe..98c851c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "devDependencies": { "@types/file-saver": "^2.0.5", "@types/lodash": "^4.14.182", + "@types/marked": "^4.0.7", "@types/node": "^17.0.31", "@types/vscode": "^1.73.1", "@typescript-eslint/eslint-plugin": "^5.30.5", @@ -20,6 +21,7 @@ "file-saver": "^2.0.5", "immutability-helper": "^3.1.1", "lodash": "^4.17.21", + "marked": "^4.2.2", "mocha": "^9.2.2", "prettier": "^2.6.2", "react": "^18.0.0", diff --git a/scripts/esbuild/watcher.js b/scripts/esbuild/watcher.js index bbdedcd..e57bdff 100644 --- a/scripts/esbuild/watcher.js +++ b/scripts/esbuild/watcher.js @@ -3,13 +3,7 @@ const { createServer } = require("http"); -function withWatcher( - config, - handleBuildFinished = () => { - /* do nothing */ - }, - port = 8880 -) { +function withWatcher(config, handleBuildFinished = undefined, port = 8880) { const watchClients = []; createServer((req, res) => { return watchClients.push( @@ -31,7 +25,9 @@ function withWatcher( if (error) { console.error(error); } else { - await handleBuildFinished(result); + if (handleBuildFinished != null) { + await handleBuildFinished(result); + } watchClients.forEach((res) => res.write("data: update\n\n")); watchClients.length = 0; } diff --git a/src/web/notebook/.gitignore b/src/web/notebook/.gitignore new file mode 100644 index 0000000..1f3072f --- /dev/null +++ b/src/web/notebook/.gitignore @@ -0,0 +1,2 @@ +/dist +/examples/*.html diff --git a/src/web/notebook/Makefile b/src/web/notebook/Makefile new file mode 100644 index 0000000..07f8b08 --- /dev/null +++ b/src/web/notebook/Makefile @@ -0,0 +1,8 @@ +all: + ./build.js + +dev: + ./build.js --development --watch + +clean: + -rm -rf dist \ No newline at end of file diff --git a/src/web/notebook/build.js b/src/web/notebook/build.js new file mode 100755 index 0000000..9d0a456 --- /dev/null +++ b/src/web/notebook/build.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/* eslint-env node */ +/* eslint @typescript-eslint/no-var-requires:0 */ + +const esbuild = require("esbuild"); +const path = require("path"); +const fs = require("fs"); +const { wasmTextPlugin } = require("../../../scripts/esbuild/wasm-text"); + +let dev = false; +let watch = false; +for (const arg of process.argv.slice(2)) { + switch (arg) { + case "--development": + dev = true; + break; + case "--watch": + watch = true; + break; + } +} + +const buildConfig = { + bundle: true, + logLevel: "info", + // target: "es6", + minify: !dev, + loader: { + ".wasm": "binary", + ".fs": "text", + }, + plugins: [wasmTextPlugin({ debug: true })], +}; + +let nbBuildConfig = { + ...buildConfig, + outdir: path.join(__dirname, "dist"), + entryPoints: [path.join(__dirname, "src", "wafnb.ts")], + publicPath: "/dist", + assetNames: "[name].txt", + sourcemap: !!dev, + loader: { + ...buildConfig.loader, + ".svg": "dataurl", + }, +}; + +let generatorBuildConfig = { + ...buildConfig, + banner: { js: "#!/usr/bin/env node" }, + platform: "node", + outfile: path.join(__dirname, "dist", "wafnb2html"), + entryPoints: [path.join(__dirname, "src", "wafnb2html.mjs")], + sourcemap: dev ? "inline" : undefined, + loader: { + ...buildConfig.loader, + ".js": "text", + ".css": "text", + }, + define: watch + ? { + WAFNB_CSS_PATH: JSON.stringify(path.join(__dirname, "/dist/wafnb.css")), + WAFNB_JS_PATH: JSON.stringify(path.join(__dirname, "/dist/wafnb.js")), + } + : undefined, +}; + +function handleGeneratorBuildFinished(result) { + return fs.chmodSync(path.join(__dirname, "dist", "wafnb2html"), "755"); +} + +if (watch) { + nbBuildConfig = { + ...nbBuildConfig, + watch: { + async onRebuild(error) { + if (error) { + console.error(error); + } + }, + }, + }; + generatorBuildConfig = { + ...generatorBuildConfig, + watch: { + async onRebuild(error, result) { + if (error) { + console.error(error); + return; + } + return handleGeneratorBuildFinished(result); + }, + }, + }; +} + +(async () => { + try { + await esbuild.build(nbBuildConfig); + await handleGeneratorBuildFinished( + await esbuild.build(generatorBuildConfig) + ); + } catch (e) { + process.exit(1); + } +})(); diff --git a/src/web/vscode-extension/examples/drawing-with-forth.wafnb b/src/web/notebook/examples/drawing-with-forth.wafnb similarity index 86% rename from src/web/vscode-extension/examples/drawing-with-forth.wafnb rename to src/web/notebook/examples/drawing-with-forth.wafnb index 5271753..f43f7c9 100644 --- a/src/web/vscode-extension/examples/drawing-with-forth.wafnb +++ b/src/web/notebook/examples/drawing-with-forth.wafnb @@ -53,18 +53,13 @@ { "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| *Tip*: Play with the numbers in `FLOWER` to create variations of the flower " }, { "kind": 2, "language": "waforth", "value": ": SQUARE ( n -- )\n 4 0 DO\n DUP FORWARD\n 90 RIGHT\n LOOP\n DROP\n;\n\n: FLOWER ( n -- )\n 24 0 DO\n DUP SQUARE\n 15 RIGHT\n LOOP\n DROP\n;\n\n250 FLOWER" }, - { - "kind": 2, - "language": "waforth", - "value": " 4 0 DO\n DUP FORWARD\n 90 RIGHT\n LOOP\n DROP\n;\n\n: FLOWER ( n -- )\n 24 0 DO\n DUP SQUARE\n 15 RIGHT\n LOOP\n DROP\n;\n\n250 FLOWER" - }, { "kind": 1, "language": "markdown", @@ -88,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| *Tip*: Change the `DEPTH` constant to make a coarser or finer grained snowflake" }, { "kind": 2, diff --git a/src/web/notebook/src/Notebook.ts b/src/web/notebook/src/Notebook.ts new file mode 100644 index 0000000..e9256f4 --- /dev/null +++ b/src/web/notebook/src/Notebook.ts @@ -0,0 +1,21 @@ +export type Notebook = { + cells: NotebookCell[]; +}; + +export type NotebookCell = { + language: string; + value: string; + kind: number; + editable?: boolean; +}; + +export function parseNotebook(contents: string): Notebook { + if (contents.trim().length === 0) { + return { cells: [] }; + } + return JSON.parse(contents); +} + +export function serializeNotebook(notebook: Notebook) { + return JSON.stringify(notebook, undefined, 2); +} diff --git a/src/web/notebook/src/generator.ts b/src/web/notebook/src/generator.ts new file mode 100644 index 0000000..773af2b --- /dev/null +++ b/src/web/notebook/src/generator.ts @@ -0,0 +1,88 @@ +import fs from "fs"; +import path from "path"; +import { parseNotebook } from "./Notebook"; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const marked = require("../../../../node_modules/marked/lib/marked.cjs").marked; + +declare let WAFNB_JS_PATH: string | undefined; +declare let WAFNB_CSS_PATH: string | undefined; + +const titleRE = /^#[^#](.*)$/; + +function escapeHTML(s: string) { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} +export async function generate({ + file, + css, + js, +}: { + bundle?: boolean; + js?: string; + css?: string; + file: string; +}) { + let outfile = file.replace(".wafnb", ".html"); + if (outfile === file) { + outfile = outfile + ".html"; + } + + let style: string; + let script: string; + if (typeof WAFNB_JS_PATH !== "undefined") { + const cssPath = path.relative(path.dirname(outfile), WAFNB_CSS_PATH!); + const jsPath = path.relative(path.dirname(outfile), WAFNB_JS_PATH!); + style = ``; + script = ``; + } else { + style = ``; + script = ``; + } + + const nb = parseNotebook( + await fs.promises.readFile(file, { + encoding: "utf-8", + flag: "r", + }) + ); + let title: string | null = null; + let out = ["
"]; + for (const cell of nb.cells) { + switch (cell.kind) { + case 1: + if (title == null) { + let m: RegExpExecArray | null = null; + for (const v of cell.value.split("\n")) { + m = titleRE.exec(v); + if (m != null) { + break; + } + } + if (m != null) { + title = m[1].trim(); + } + } + out.push( + "
" + marked.parse(cell.value) + "
" + ); + break; + case 2: + out.push( + "
" + + escapeHTML(cell.value) + + "
" + ); + break; + default: + throw new Error("unexpected kind"); + } + } + out.push("
"); + + 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 new file mode 100644 index 0000000..51c59cd --- /dev/null +++ b/src/web/notebook/src/wafnb.css @@ -0,0 +1,21 @@ +body { + font-family: sans-serif; +} + +.content { + max-width: 40em; + margin-left: auto; + margin-right: auto; +} + +code.raw-code-cell { + font-family: monospace; + padding: 0.5em 1em; + display: block; + white-space: pre; + background-color: #eee; +} + +.editor { + flex: 1; +} diff --git a/src/web/notebook/src/wafnb.ts b/src/web/notebook/src/wafnb.ts new file mode 100644 index 0000000..b3402a4 --- /dev/null +++ b/src/web/notebook/src/wafnb.ts @@ -0,0 +1,12 @@ +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/wafnb2html.mjs b/src/web/notebook/src/wafnb2html.mjs new file mode 100644 index 0000000..4038659 --- /dev/null +++ b/src/web/notebook/src/wafnb2html.mjs @@ -0,0 +1,13 @@ +import wafnbJS from "../dist/wafnb.js"; +import wafnbCSS from "../dist/wafnb.css"; +import { generate } from "./generator"; +import process from "process"; + +(async () => { + const file = process.argv[2]; + if (file == null) { + console.error("missing file"); + process.exit(-1); + } + await generate({ file, js: wafnbJS, css: wafnbCSS }); +})(); diff --git a/src/web/vscode-extension/README.md b/src/web/vscode-extension/README.md index 7747a0d..76de175 100644 --- a/src/web/vscode-extension/README.md +++ b/src/web/vscode-extension/README.md @@ -8,15 +8,15 @@ Logo-like Forth Turtle graphics.
-WAForth notebook +WAForth notebook
-
WAForth notebook
+
WAForth notebook
## Installation & Usage Install the extension from the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=remko.waforth-vscode-extension). -Once the extension is installed, you can create a new WAForth notebook from the command pallet (*WAForth: New notebook*), or open [this example notebook](https://raw.githubusercontent.com/remko/waforth/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb). +Once the extension is installed, you can create a new WAForth notebook from the command pallet (*WAForth: New notebook*), or open [this example notebook](https://raw.githubusercontent.com/remko/waforth/master/src/web/notebook/examples/drawing-with-forth.wafnb). The extension also works in the web-based VS Code environments. -For example, install the extension on https://github.dev, and then open [this example notebook](https://github.dev/remko/waforth/blob/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb). +For example, install the extension on https://github.dev, and then open [this example notebook](https://github.dev/remko/waforth/blob/master/src/web/notebook/examples/drawing-with-forth.wafnb). diff --git a/src/web/vscode-extension/package.json b/src/web/vscode-extension/package.json index 115fe05..8643540 100644 --- a/src/web/vscode-extension/package.json +++ b/src/web/vscode-extension/package.json @@ -1,6 +1,6 @@ { "name": "waforth-vscode-extension", - "version": "0.1.6", + "version": "0.1.7", "displayName": "WAForth", "description": "WAForth interactive notebooks", "categories": [ diff --git a/src/web/vscode-extension/src/extension.ts b/src/web/vscode-extension/src/extension.ts index f26826c..ea5e3ad 100644 --- a/src/web/vscode-extension/src/extension.ts +++ b/src/web/vscode-extension/src/extension.ts @@ -2,6 +2,11 @@ import * as vscode from "vscode"; import WAForth, { ErrorCode, isSuccess } from "../../waforth"; import draw from "../../thurtle/draw"; import JSJSX from "thurtle/jsjsx"; +import { + parseNotebook, + Notebook, + serializeNotebook, +} from "../../notebook/src/Notebook"; export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( @@ -41,30 +46,15 @@ export function deactivate() { // Notebook Serializer ////////////////////////////////////////////////// -interface RawNotebookData { - cells: RawNotebookCell[]; -} - -interface RawNotebookCell { - language: string; - value: string; - kind: vscode.NotebookCellKind; - editable?: boolean; -} - export class NotebookSerializer implements vscode.NotebookSerializer { public readonly label: string = "WAForth Content Serializer"; public async deserializeNotebook( data: Uint8Array ): Promise { - const contents = new TextDecoder().decode(data); - if (contents.trim().length === 0) { - return new vscode.NotebookData([]); - } - let raw: RawNotebookData; + let raw: Notebook; try { - raw = JSON.parse(contents); + raw = parseNotebook(new TextDecoder().decode(data)); } catch (e) { vscode.window.showErrorMessage("Error parsing:", (e as any).message); raw = { cells: [] }; @@ -79,7 +69,7 @@ export class NotebookSerializer implements vscode.NotebookSerializer { public async serializeNotebook( data: vscode.NotebookData ): Promise { - const contents: RawNotebookData = { cells: [] }; + const contents: Notebook = { cells: [] }; for (const cell of data.cells) { contents.cells.push({ kind: cell.kind, @@ -87,12 +77,12 @@ export class NotebookSerializer implements vscode.NotebookSerializer { value: cell.value, }); } - return new TextEncoder().encode(JSON.stringify(contents, undefined, 2)); + return new TextEncoder().encode(serializeNotebook(contents)); } } ////////////////////////////////////////////////// -// Notebook Serializer +// Notebook Controller ////////////////////////////////////////////////// async function createNotebookController( diff --git a/yarn.lock b/yarn.lock index 6883e55..2523c34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -112,6 +112,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== +"@types/marked@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.7.tgz#400a76809fd08c2bbd9e25f3be06ea38c8e0a1d3" + integrity sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw== + "@types/node@^17.0.31": version "17.0.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d" @@ -1307,6 +1312,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +marked@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.2.tgz#1d2075ad6cdfe42e651ac221c32d949a26c0672a" + integrity sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"