notebook: Move some files

This commit is contained in:
Remko Tronçon 2022-11-20 09:20:35 +01:00
parent 3494e15a65
commit a29e0f9567
17 changed files with 307 additions and 42 deletions

View file

@ -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

View file

@ -164,9 +164,9 @@ Because it is powered by WebAssembly, this extension works both in the desktop v
<div align="center">
<div>
<a href="https://github.dev/remko/waforth/blob/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb"><img src="https://raw.githubusercontent.com/remko/waforth/master/src/web/vscode-extension/doc/notebook.gif" alt="WAForth notebook"></a>
<a href="https://github.dev/remko/waforth/blob/master/src/web/notebook/examples/drawing-with-forth.wafnb"><img src="https://raw.githubusercontent.com/remko/waforth/master/src/web/vscode-extension/doc/notebook.gif" alt="WAForth notebook"></a>
</div>
<figcaption><em><a href="https://github.dev/remko/waforth/blob/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb">WAForth notebook</a></em></figcaption>
<figcaption><em><a href="https://github.dev/remko/waforth/blob/master/src/web/notebook/examples/drawing-with-forth.wafnb">WAForth notebook</a></em></figcaption>
</div>

View file

@ -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",

View file

@ -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;
}

2
src/web/notebook/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/dist
/examples/*.html

View file

@ -0,0 +1,8 @@
all:
./build.js
dev:
./build.js --development --watch
clean:
-rm -rf dist

106
src/web/notebook/build.js Executable file
View file

@ -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);
}
})();

View file

@ -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,

View file

@ -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);
}

View file

@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 = `<link rel="stylesheet" href="${cssPath}">`;
script = `<script type="application/javascript" src="${jsPath}"></script>`;
} else {
style = `<style>${css}</style>`;
script = `<script type="application/javascript">${js}</script>`;
}
const nb = parseNotebook(
await fs.promises.readFile(file, {
encoding: "utf-8",
flag: "r",
})
);
let title: string | null = null;
let out = ["<div class='content'>"];
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(
"<div class='text-cell'>" + marked.parse(cell.value) + "</div>"
);
break;
case 2:
out.push(
"<div data-hook='code-cell' class='raw-code-cell'>" +
escapeHTML(cell.value) +
"</div>"
);
break;
default:
throw new Error("unexpected kind");
}
}
out.push("</div>");
out = [`<title>${escapeHTML(title ?? "")}</title>`, style, ...out, script];
return fs.promises.writeFile(outfile, out.join("\n"));
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -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 });
})();

View file

@ -8,15 +8,15 @@ Logo-like Forth Turtle graphics.
<div align="center">
<div>
<a href="https://github.dev/remko/waforth/blob/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb"><img src="https://raw.githubusercontent.com/remko/waforth/master/src/web/vscode-extension/doc/notebook.gif" alt="WAForth notebook"></a>
<a href="https://github.dev/remko/waforth/blob/master/src/web/notebook/examples/drawing-with-forth.wafnb"><img src="https://raw.githubusercontent.com/remko/waforth/master/src/web/notebook/doc/notebook.gif" alt="WAForth notebook"></a>
</div>
<figcaption><em><a href="https://github.dev/remko/waforth/blob/master/src/web/vscode-extension/examples/drawing-with-forth.wafnb">WAForth notebook</a></em></figcaption>
<figcaption><em><a href="https://github.dev/remko/waforth/blob/master/src/web/notebook/examples/drawing-with-forth.wafnb">WAForth notebook</a></em></figcaption>
</div>
## 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).

View file

@ -1,6 +1,6 @@
{
"name": "waforth-vscode-extension",
"version": "0.1.6",
"version": "0.1.7",
"displayName": "WAForth",
"description": "WAForth interactive notebooks",
"categories": [

View file

@ -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<vscode.NotebookData> {
const contents = new TextDecoder().decode(data);
if (contents.trim().length === 0) {
return new vscode.NotebookData([]);
}
let raw: RawNotebookData;
let raw: Notebook;
try {
raw = <RawNotebookData>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<Uint8Array> {
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(

View file

@ -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"