Adding jotblk sample.

A web editor that embeds uEforth.
This commit is contained in:
Brad Nelson
2025-05-10 12:41:18 -07:00
parent ac6abf445b
commit 3a5daad8af
11 changed files with 751 additions and 0 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
out
build.ninja
attic/jotblk/env

12
attic/jotblk/Makefile Executable file
View File

@ -0,0 +1,12 @@
all:
./run.sh
deploy:
gcloud app deploy -q --project jotblk *.yaml
setup:
python3 -m venv env
env/bin/pip install -r requirements.txt
datastore:
echo "Run: . dstore.sh"

20
attic/jotblk/README.md Normal file
View File

@ -0,0 +1,20 @@
# jotblk
A web based block based editor to run on AppEngine.
To run you need the AppEngine SDK.
To setup run:
```
make setup
```
In parallel run (for the datastore):
```
. dstore.sh
```
And separately (to run a webserver on http://localhost:8080):
```
./run.sh
```

17
attic/jotblk/app.yaml Normal file
View File

@ -0,0 +1,17 @@
runtime: python39
default_expiration: "10m"
handlers:
- url: /
static_files: static/index.html
upload: static/index.html
secure: always
- url: /static
static_dir: static
secure: always
- url: /io
script: main.app
secure: always

2
attic/jotblk/dstore.sh Normal file
View File

@ -0,0 +1,2 @@
# Run this to start an datastore emulator.
gcloud emulators firestore start --database-mode=datastore-mode --project=jotblk --host-port=127.0.0.1:8099

81
attic/jotblk/main.py Normal file
View File

@ -0,0 +1,81 @@
# Copyright 2025 Bradley D. Nelson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
from flask import Flask, request, send_from_directory
from google.cloud import datastore
client = datastore.Client()
root_passwd_key = client.key('passwd', 'root')
root_passwd_entity = client.get(root_passwd_key)
if root_passwd_entity is None:
root_passwd = 'xyzzy'
else:
root_passwd = root_passwd_entity['secret']
def SaveBlock(index, data):
assert index >= 0
assert len(data) == 1024, len(data)
entity = datastore.Entity(key=client.key('block', index+1))
entity.update({'data': data})
client.put(entity)
return ''
def LoadBlocks(start, end):
assert end >= end
assert start >= 0
assert end - start <= 128
query = client.query(kind='block')
first_key = client.key('block', start+1)
last_key = client.key('block', end+1)
query.key_filter(first_key, '>=')
query.key_filter(last_key, '<')
entities = query.fetch()
blks = {}
for i in entities:
blks[i.key.path[0]['id']-1] = i['data']
result = []
for i in range(start, end+1):
if i in blks:
result.append(blks[i])
else:
result.append(b' ' * 1024)
return b''.join(result)
app = Flask(__name__)
@app.route('/<path:filename>')
def canned(filename):
return send_from_directory('static', filename)
@app.route('/')
def root():
return canned('index.html')
@app.route('/io', methods=['POST'])
def io():
if root_passwd != request.form['passwd']:
return 'deny', 403
if request.form['command'] == 'read':
start = int(request.form['start'])
end = int(request.form['end'])
return LoadBlocks(start, end)
elif request.form['command'] == 'write':
index = int(request.form['index'])
data = request.files['data'].read()
return SaveBlock(index, data)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)

View File

@ -0,0 +1,2 @@
Flask==3.0.0
google-cloud-datastore==2.15.1

8
attic/jotblk/run.sh Executable file
View File

@ -0,0 +1,8 @@
#! /bin/bash
export DATASTORE_DATASET=jotblk
export DATASTORE_EMULATOR_HOST=127.0.0.1:8099
export DATASTORE_EMULATOR_HOST_PATH=127.0.0.1:8099/datastore
export DATASTORE_HOST=http://127.0.0.1:8099
export DATASTORE_PROJECT_ID=jotblk
env/bin/python3 main.py

View File

@ -0,0 +1,3 @@
<!DOCTYPE html>
<body>
<script src="jotblk.js"></script>

View File

@ -0,0 +1,562 @@
// Copyright 2025 Bradley D. Nelson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
const MAX_BLOCKS = 16 * 1024;
const SYNC_CHUNK = 64;
const DIRTY = 1;
const LOADED = 2;
var blocks = new Uint8Array(1024 * MAX_BLOCKS);
var flags = new Uint8Array(MAX_BLOCKS);
var clipboard = [];
var screen_history = [];
var keymap = {};
var scr = 0;
var pos = 0;
var marker = 0;
var passwd = '';
var ForthKeyDown = null;
var ForthKeyPress = null;
var forth_added = false;
var ueforth = null;
document.body.style.overflow = 'hidden';
document.body.style.margin = '0';
document.body.style.border = '0';
document.body.style.padding = '0';
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
function SpaceIt() {
if (!(flags[scr] & LOADED)) {
return;
}
if (blocks[scr * 1024] == 0) {
for (var i = 0; i < 1024; ++i) {
blocks[i + scr * 1024] = 32;
}
flags[scr] |= DIRTY;
}
}
function IsEmpty(n) {
for (var i = 0; i < 1024; i++) {
var ch = blocks[i + n * 1024];
if (ch != 32 && ch != 0) {
return false;
}
}
return true;
}
function FindEnd() {
var i = pos;
while (i < 1023 &&
(blocks[scr * 1024 + i] != 32 ||
blocks[scr * 1024 + i + 1] != 32)) {
i++;
}
return i;
}
function Insert() {
var end = FindEnd();
for (var i = end; i >= pos; i--) {
blocks[scr * 1024 + i + 1] = blocks[scr * 1024 + i];
}
blocks[scr * 1024 + pos] = 32;
flags[scr] |= DIRTY;
}
function Delete() {
var end = FindEnd();
for (var i = pos; i < end; i++) {
blocks[scr * 1024 + i] = blocks[scr * 1024 + i + 1];
}
flags[scr] |= DIRTY;
}
function FindSpan() {
var i = Math.floor(pos / 64) * 64;
var min = 63;
var max = 0;
for (var j = 0; j < 64; j++) {
if (blocks[scr * 1024 + i + j] != 32) {
min = Math.min(min, j);
max = Math.max(max, j);
}
}
if (min > max) {
return [i, i + 63];
}
return [i + min, i + max];
}
function Home() {
var span = FindSpan();
var start = Math.floor(pos / 64) * 64;
if (pos == span[0]) {
pos = start;
} else {
pos = span[0];
}
}
function End() {
var span = FindSpan();
var end = Math.floor(pos / 64) * 64 + 63;
var span1 = Math.min(span[1] + 1, end);
if (pos == span1) {
pos = end;
} else {
pos = span1;
}
}
function Up() {
if (pos >= 64) {
pos -= 64;
}
}
function Down() {
if (pos + 64 < 1024) {
pos += 64;
}
}
function Left() {
pos = Math.max(0, pos - 1);
}
function Right() {
pos = Math.min(1023, pos + 1);
}
function Copy() {
var row = Math.floor(pos / 64) * 64;
clipboard.push(blocks.slice(scr * 1024 + row, scr * 1024 + row + 64));
Up();
}
function Cut() {
var row = Math.floor(pos / 64) * 64;
clipboard.push(blocks.slice(scr * 1024 + row, scr * 1024 + row + 64));
for (var j = 0; j < 64; j++) {
blocks[scr * 1024 + row + j] = 32;
}
flags[scr] |= DIRTY;
Up();
}
function Paste() {
if (clipboard.length == 0) {
return;
}
var row = Math.floor(pos / 64) * 64;
var data = clipboard.pop();
for (var j = 0; j < 64; j++) {
blocks[scr * 1024 + row + j] = data[j];
}
flags[scr] |= DIRTY;
Down();
}
function Update() {
SpaceIt();
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = '16px consolas, Monaco, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.save();
ctx.scale(canvas.width / 64 , canvas.height / 17);
for (var j = 0; j < 16; ++j) {
for (var i = 0; i < 64; ++i) {
if (pos === i + j * 64) {
ctx.fillStyle = '#750';
ctx.fillRect(i, j, 1, 1);
}
}
}
ctx.save();
var m = ctx.measureText('W');
var w = m.width;
var h = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent;
ctx.scale(1 / w, 1 / h);
for (var j = 0; j < 16; ++j) {
for (var i = 0; i < 64; ++i) {
var ch = String.fromCharCode(blocks[i + j * 64 + scr * 1024]);
ctx.fillStyle = '#fb0';
ctx.fillText(ch, (i + 0.5) * w, (j + 0.5) * h);
}
}
ctx.fillStyle = '#750';
ctx.textAlign = 'right';
var info = '';
if (flags[scr] & DIRTY) {
info += 'D ';
}
if (!(flags[scr] & LOADED)) {
info += 'L ';
}
info += scr;
ctx.fillText(info, 63.5 * w, 16.5 * h);
ctx.restore();
ctx.restore();
}
function LoadChunk(n) {
if ((flags[n * SYNC_CHUNK] & LOADED)) {
return Promise.resolve();
} else {
return LoadBlocks(n * SYNC_CHUNK, (n + 1) * SYNC_CHUNK).then(function() {
Update();
});
}
}
function MaybeLoad() {
var s = Math.floor(scr / SYNC_CHUNK);
return LoadChunk(s);
}
function LineString(blk, row) {
var result = '';
for (var col = 0; col < 64; col++) {
result += String.fromCharCode(blocks[blk * 1024 + row * 64 + col]);
}
return result;
}
function BlockString(blk, linebreaks) {
var result = '';
for (var row = 0; row < 16; row++) {
result += LineString(blk, row);
if (linebreaks) {
result += '\n';
}
}
return result;
}
function Eval(n) {
eval(BlockString(n, true));
}
function Print() {
var start = Math.min(marker, scr);
var end = Math.max(marker, scr);
var content = '';
content += '<!DOCTYPE html>\n';
for (var i = start; i <= end; i++) {
content += '<pre style="border: 1px solid; display: inline-block;">\n';
content += BlockString(i, true);
content += '<hr/>' + i;
content += '</pre><br/>\n';
}
var blob = new Blob([content], { type: 'text/html' });
var url = URL.createObjectURL(blob);
window.open(url, '_blank');
}
function Backspace() {
if (pos > 0) {
--pos;
Delete();
}
}
function Goto(n) {
scr = n;
MaybeLoad();
}
function Gosub(n) {
screen_history.push(scr);
Goto(n);
}
function Adjust(n) {
Goto(Math.max(0, Math.min(MAX_BLOCKS - 1, scr + n)));
}
function Enter() {
pos = Math.floor((pos + 64) / 64) * 64;
if (pos > 1023) {
pos -= 64;
}
}
function ShiftUp() {
pos = (pos % 64);
}
function ShiftDown() {
pos = (pos % 64) + 15 * 64;
}
function GetLink() {
var s = BlockString(scr, false);
var at = s.indexOf('@', pos);
var paren = s.indexOf(' )', pos);
if (at >= 0 && (at < paren || paren < 0)) {
return s.slice(at).split(' ')[0];
}
if (paren >= 0) {
var p2 = s.lastIndexOf('( ', paren);
if (p2 >= 0) {
return s.slice(p2, paren + 2);
}
}
return '';
}
function Find(s) {
for (var i = 0; i < MAX_BLOCKS; i++) {
var j = (scr + 1 + i) % MAX_BLOCKS;
if (BlockString(j, false).indexOf(s) >= 0) {
return j;
}
}
return null;
}
function FollowLink() {
var link = GetLink();
if (link.startsWith('@')) {
var n = parseInt(link.slice(1));
Gosub(n);
} else if (link.startsWith('( ')) {
var n = Find(link.slice(2, -2));
if (n !== null) {
Gosub(n);
}
}
}
function Type(ch) {
Insert();
blocks[pos + scr * 1024] = ch;
pos = Math.min(1023, pos + 1);
}
function Key(e) {
if (e.ctrlKey && keymap['^' + e.key]) {
keymap['^' + e.key](e);
} else if (e.shiftKey && keymap['+' + e.key]) {
keymap['+' + e.key](e);
} else if (keymap[e.key]) {
keymap[e.key](e);
} else if (e.key.length == 1 && !e.ctrlKey) {
Type(e.key.charCodeAt(0));
} else {
return true;
}
Update();
e.preventDefault();
return false;
}
function Login(e) {
if (e.key == 'Backspace') {
passwd = passwd.slice(0, -1);
} else if (e.key == 'Enter') {
window.onkeydown = Key;
MaybeLoad().then(function() {
Eval(63);
});
} else if (e.key.length == 1) {
passwd += e.key;
}
Update();
e.preventDefault();
return false;
}
function Resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
Update();
}
function SaveBlock(i) {
var fd = new FormData();
fd.append('command', 'write');
fd.append('passwd', passwd);
fd.append('index', i);
fd.append('data', new Blob([blocks.slice(i * 1024, (i + 1) * 1024)],
{type: "application/octet-stream"}));
return fetch('/io', {'method': 'POST', body: fd}).then(function() {
flags[i] &= ~DIRTY;
Update();
});
}
function Sync() {
for (var i = 0; i < MAX_BLOCKS; ++i) {
if ((flags[i] & LOADED) && (flags[i] & DIRTY)) {
SaveBlock(i);
}
}
}
function FindEmpty() {
screen_history.push(scr);
while (!IsEmpty(scr)) {
Adjust(1);
}
}
function StampLog() {
var now = new Date();
var month = ('0' + (1 + now.getMonth())).slice(-2);
var day = ('0' + now.getDate()).slice(-2);
var dt = now.getFullYear() + '-' + month + '-' + day;
for (var i = 0; i < dt.length; i++) {
blocks[54 + i + scr * 1024] = dt.charCodeAt(i);
}
var mark = 'LOG: ';
for (var i = 0; i < mark.length; i++) {
blocks[i + scr * 1024] = mark.charCodeAt(i);
}
pos = mark.length;
flags[scr] |= DIRTY;
}
function Back() {
if (screen_history.length) {
Goto(screen_history.pop());
}
}
function LoadBlocks(start, end) {
var fd = new FormData();
fd.append('command', 'read');
fd.append('passwd', passwd);
fd.append('start', start);
fd.append('end', end);
return fetch('/io', {'method': 'POST', body: fd}).then(function(response) {
if (!response.ok) {
throw 'bad fetch';
}
return response.arrayBuffer().then(function(data) {
var u8 = new Uint8Array(data);
if (u8.length != (end + 1 - start) * 1024) {
throw 'bad load';
}
for (var i = start; i < end; i++) {
if (flags[i] & LOADED) {
return;
}
var dst = i * 1024;
var src = (i - start) * 1024;
for (var j = 0; j < 1024; j++) {
blocks[j + dst] = u8[j + src];
}
flags[i] |= LOADED;
}
});
});
}
function ForthKeyFilter(e) {
if (e.key == 'f' && e.ctrlKey) {
ToggleForth();
e.preventDefault();
return false;
}
return ForthKeyDown(e);
}
function ToggleForth() {
if (!forth_added) {
forth_added = true;
var fscript = document.createElement('script');
fscript.src = 'myforth.fs';
fscript.type = 'text/forth';
document.body.appendChild(fscript);
var script = document.createElement('script');
script.src = 'https://eforth.appspot.com/ueforth.js';
document.body.appendChild(script);
function Loader() {
if (ueforth !== null) {
ueforth.Start();
canvas.style.display = 'none';
setTimeout(function() {
ForthKeyDown = window.onkeydown;
ForthKeyPress = window.onkeypress;
window.onkeydown = ForthKeyFilter;
}, 500);
} else {
setTimeout(Loader, 10);
}
}
Loader();
return;
}
if (window.onkeydown === Key) {
window.onkeydown = ForthKeyFilter;
window.onkeypress = ForthKeyPress;
canvas.style.display = 'none';
ueforth.screen.style.display = '';
} else {
window.onkeydown = Key;
window.onkeypress = null;
ueforth.screen.style.display = 'none';
canvas.style.display = '';
Resize();
}
}
function Init() {
keymap['Delete'] = Delete;
keymap['Backspace'] = Backspace;
keymap['PageUp'] = function() { Adjust(-1); };
keymap['PageDown'] = function() { Adjust(1); };
keymap['+PageUp'] = function() { Adjust(-16); };
keymap['+PageDown'] = function() { Adjust(16); };
keymap['Home'] = Home;
keymap['End'] = End;
keymap['Enter'] = Enter;
keymap['^Enter'] = FollowLink;
keymap['ArrowUp'] = Up;
keymap['ArrowDown'] = Down;
keymap['ArrowLeft'] = Left;
keymap['ArrowRight'] = Right;
keymap['+ArrowUp'] = ShiftUp;
keymap['+ArrowDown'] = ShiftDown;
keymap['+ArrowLeft'] = Home;
keymap['+ArrowRight'] = End;
keymap['^c'] = Copy;
keymap['^x'] = Cut;
keymap['^v'] = Paste;
keymap['^m'] = function() { marker = scr; };
keymap['^p'] = Print;
keymap['^g'] = function() { Eval(scr); };
keymap['^o'] = FindEmpty;
keymap['^l'] = StampLog;
keymap['^b'] = Back;
keymap['^f'] = ToggleForth;
keymap['^h'] = function() { Gosub(0); };
window.addEventListener('resize', Resize);
window.onkeydown = Login;
Resize();
}
Init();
setInterval(Sync, 3000);

View File

@ -0,0 +1,43 @@
\ Copyright 2021 Bradley D. Nelson
\
\ Licensed under the Apache License, Version 2.0 (the "License");
\ you may not use this file except in compliance with the License.
\ You may obtain a copy of the License at
\
\ http://www.apache.org/licenses/LICENSE-2.0
\
\ Unless required by applicable law or agreed to in writing, software
\ distributed under the License is distributed on an "AS IS" BASIS,
\ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\ See the License for the specific language governing permissions and
\ limitations under the License.
web definitions
JSWORD: block-read { n a -- }
for (var i = 0; i < 1024; i++) {
u8[a + i] = blocks[n * 1024 + i];
}
~
also internals
create block-buffer 1024 allot
also forth definitions
variable scr
: block ( n -- a ) block-buffer block-read block-buffer ;
: buffer ( n -- a ) block ;
only forth definitions
( Loading )
: load ( n -- ) block 1024 evaluate ;
: thru ( a b -- ) over - 1+ for aft dup >r load r> 1+ then next drop ;
( Listing )
: list ( n -- ) scr ! ." Block " scr @ . cr scr @ block
15 for dup 63 type [char] | emit space 15 r@ - . cr 64 + next drop ;
only forth definitions