tapmach 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/index.js +565 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# tapmach
|
|
2
|
+
|
|
3
|
+
**tapmach** tape-based programming language designed for visual execution and extreme simplicity. It focuses on manipulating a dynamic array of cells through a clean, terminal-inspired interface.
|
|
4
|
+
|
|
5
|
+
>[!note]
|
|
6
|
+
> All the claims of performance are restrained by the execution speed of javascript. So don't really think this can beat C or other high performance programming languages.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
### 🚀 Getting Started
|
|
11
|
+
|
|
12
|
+
To run the Tapmach environment, ensure you have **Bun** installed, then execute:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun run index.ts
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The server will start at `http://localhost:3000`. Use the built-in editor to write your code and hit the **Run** button to see the tape come to life. To stop the server, press `q` in your terminal.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### 📜 Language Reference
|
|
23
|
+
|
|
24
|
+
Tapmach operates on a **Tape** (a sequence of cells). You interact with the "current" cell by creating, modifying, or destroying it.
|
|
25
|
+
|
|
26
|
+
| Instruction | Description |
|
|
27
|
+
| :--- | :--- |
|
|
28
|
+
| `create_cell` | Appends a new cell to the end of the tape with an initial value of `0`. |
|
|
29
|
+
| `add <number>` | Adds the specified integer to the value of the currently active cell. |
|
|
30
|
+
| `destroy_cell`| Removes the current cell from the tape with a visual collapse animation. |
|
|
31
|
+
|
|
32
|
+
>[!note]
|
|
33
|
+
> Every line in Tapmach is treated as a distinct operation followed by a 1-second delay to allow the visual animations to complete.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### 🛠Technical Architecture
|
|
38
|
+
|
|
39
|
+
The project is built with a high-performance, "HTML-First" mindset:
|
|
40
|
+
|
|
41
|
+
* **Runtime:** Powered by **Bun** for fast I/O and dev-server capabilities.
|
|
42
|
+
* **Frontend:** Pure HTML/CSS/JS.
|
|
43
|
+
* **Styling:** Features a minimalist UI.
|
|
44
|
+
* **State:** Code is automatically persisted to `localStorage` using a debounced saving mechanism, ensuring you never lose your work.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 🎨 Design Goals
|
|
49
|
+
|
|
50
|
+
* **Minimalist UI:** No clutter, just the tape and the code.
|
|
51
|
+
* **Performance:** Lightweight execution with minimal dependencies (`ansis` for terminal styling and `Bun.serve` for the backend).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
|
|
34
|
+
// node_modules/ansis/index.cjs
|
|
35
|
+
var require_ansis = __commonJS((exports, module) => {
|
|
36
|
+
var e;
|
|
37
|
+
var t;
|
|
38
|
+
var r;
|
|
39
|
+
var { defineProperty: l, setPrototypeOf: n, create: o, keys: s } = Object;
|
|
40
|
+
var i = "";
|
|
41
|
+
var { round: c, max: a } = Math;
|
|
42
|
+
var p = (e2) => {
|
|
43
|
+
let t2 = /([a-f\d]{3,6})/i.exec(e2)?.[1], r2 = t2?.length, l2 = parseInt(6 ^ r2 ? 3 ^ r2 ? "0" : t2[0] + t2[0] + t2[1] + t2[1] + t2[2] + t2[2] : t2, 16);
|
|
44
|
+
return [l2 >> 16 & 255, l2 >> 8 & 255, 255 & l2];
|
|
45
|
+
};
|
|
46
|
+
var u = (e2, t2, r2) => e2 ^ t2 || t2 ^ r2 ? 16 + 36 * c(e2 / 51) + 6 * c(t2 / 51) + c(r2 / 51) : 8 > e2 ? 16 : e2 > 248 ? 231 : c(24 * (e2 - 8) / 247) + 232;
|
|
47
|
+
var d = (e2) => {
|
|
48
|
+
let t2, r2, l2, n2, o2;
|
|
49
|
+
return 8 > e2 ? 30 + e2 : 16 > e2 ? e2 - 8 + 90 : (232 > e2 ? (o2 = (e2 -= 16) % 36, t2 = (e2 / 36 | 0) / 5, r2 = (o2 / 6 | 0) / 5, l2 = o2 % 6 / 5) : t2 = r2 = l2 = (10 * (e2 - 232) + 8) / 255, n2 = 2 * a(t2, r2, l2), n2 ? 30 + (c(l2) << 2 | c(r2) << 1 | c(t2)) + (2 ^ n2 ? 0 : 60) : 30);
|
|
50
|
+
};
|
|
51
|
+
var f = (() => {
|
|
52
|
+
let r2 = (e2) => o2.some((t2) => e2.test(t2)), l2 = globalThis, n2 = l2.process ?? {}, o2 = n2.argv ?? [], i2 = n2.env ?? {}, c2 = -1;
|
|
53
|
+
try {
|
|
54
|
+
e = "," + s(i2).join(",");
|
|
55
|
+
} catch (e2) {
|
|
56
|
+
i2 = {}, c2 = 0;
|
|
57
|
+
}
|
|
58
|
+
let a2 = "FORCE_COLOR", p2 = { false: 0, 0: 0, 1: 1, 2: 2, 3: 3 }[i2[a2]] ?? -1, u2 = a2 in i2 && p2 || r2(/^--color=?(true|always)?$/);
|
|
59
|
+
return u2 && (c2 = p2), ~c2 || (c2 = ((r3, l3, n3) => (t = r3.TERM, { "24bit": 3, truecolor: 3, ansi256: 2, ansi: 1 }[r3.COLORTERM] || (r3.CI ? /,GITHUB/.test(e) ? 3 : 1 : l3 && t !== "dumb" ? n3 ? 3 : /-256/.test(t) ? 2 : 1 : 0)))(i2, !!i2.PM2_HOME || i2.NEXT_RUNTIME?.includes("edge") || !!n2.stdout?.isTTY, n2.platform === "win32")), !p2 || i2.NO_COLOR || r2(/^--(no-color|color=(false|never))$/) ? 0 : l2.window?.chrome || u2 && !c2 ? 3 : c2;
|
|
60
|
+
})();
|
|
61
|
+
var g = { open: i, close: i };
|
|
62
|
+
var h = 39;
|
|
63
|
+
var b = 49;
|
|
64
|
+
var O = {};
|
|
65
|
+
var m = ({ p: e2 }, { open: t2, close: l2 }) => {
|
|
66
|
+
let o2 = (e3, ...r2) => {
|
|
67
|
+
if (!e3) {
|
|
68
|
+
if (t2 && t2 === l2)
|
|
69
|
+
return t2;
|
|
70
|
+
if ((e3 ?? i) === i)
|
|
71
|
+
return i;
|
|
72
|
+
}
|
|
73
|
+
let n2, s3 = e3.raw ? String.raw({ raw: e3 }, ...r2) : i + e3, c3 = o2.p, a2 = c3.o, p2 = c3.c;
|
|
74
|
+
if (s3.includes("\x1B"))
|
|
75
|
+
for (;c3; c3 = c3.p) {
|
|
76
|
+
let { open: e4, close: t3 } = c3, r3 = t3.length, l3 = i, o3 = 0;
|
|
77
|
+
if (r3)
|
|
78
|
+
for (;~(n2 = s3.indexOf(t3, o3)); o3 = n2 + r3)
|
|
79
|
+
l3 += s3.slice(o3, n2) + e4;
|
|
80
|
+
s3 = l3 + s3.slice(o3);
|
|
81
|
+
}
|
|
82
|
+
return a2 + (s3.includes(`
|
|
83
|
+
`) ? s3.replace(/(\r?\n)/g, p2 + "$1" + a2) : s3) + p2;
|
|
84
|
+
}, s2 = t2, c2 = l2;
|
|
85
|
+
return e2 && (s2 = e2.o + t2, c2 = l2 + e2.c), n(o2, r), o2.p = { open: t2, close: l2, o: s2, c: c2, p: e2 }, o2.open = s2, o2.close = c2, o2;
|
|
86
|
+
};
|
|
87
|
+
var w = new function e2(t2 = f) {
|
|
88
|
+
let s2 = { Ansis: e2, level: t2, isSupported: () => a2, strip: (e3) => e3.replace(/[\u001B\u009B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, i), extend(e3) {
|
|
89
|
+
for (let t3 in e3) {
|
|
90
|
+
let r2 = e3[t3], l2 = (typeof r2)[0];
|
|
91
|
+
l2 === "s" ? (c2(t3, T(...p(r2))), c2(_(t3), v(...p(r2)))) : c2(t3, r2, l2 === "f");
|
|
92
|
+
}
|
|
93
|
+
return r = o({}, O), n(s2, r), s2;
|
|
94
|
+
} }, c2 = (e3, t3, r2) => {
|
|
95
|
+
O[e3] = { get() {
|
|
96
|
+
let n2 = r2 ? (...e4) => m(this, t3(...e4)) : m(this, t3);
|
|
97
|
+
return l(this, e3, { value: n2 }), n2;
|
|
98
|
+
} };
|
|
99
|
+
}, a2 = t2 > 0, w2 = (e3, t3) => a2 ? { open: `\x1B[${e3}m`, close: `\x1B[${t3}m` } : g, y = (e3) => (t3) => e3(...p(t3)), R = (e3, t3) => (r2, l2, n2) => w2(`${e3}8;2;${r2};${l2};${n2}`, t3), $ = (e3, t3) => (r2, l2, n2) => w2(((e4, t4, r3) => d(u(e4, t4, r3)))(r2, l2, n2) + e3, t3), x = (e3) => (t3, r2, l2) => e3(u(t3, r2, l2)), T = R(3, h), v = R(4, b), C = (e3) => w2("38;5;" + e3, h), E = (e3) => w2("48;5;" + e3, b);
|
|
100
|
+
t2 === 2 ? (T = x(C), v = x(E)) : t2 === 1 && (T = $(0, h), v = $(10, b), C = (e3) => w2(d(e3), h), E = (e3) => w2(d(e3) + 10, b));
|
|
101
|
+
let M, I = { fg: C, bg: E, rgb: T, bgRgb: v, hex: y(T), bgHex: y(v), visible: g, reset: w2(0, 0), bold: w2(1, 22), dim: w2(2, 22), italic: w2(3, 23), underline: w2(4, 24), inverse: w2(7, 27), hidden: w2(8, 28), strikethrough: w2(9, 29) }, _ = (e3) => "bg" + e3[0].toUpperCase() + e3.slice(1), k = "Bright";
|
|
102
|
+
return "black,red,green,yellow,blue,magenta,cyan,white,gray".split(",").map((e3, t3) => {
|
|
103
|
+
M = _(e3), 8 > t3 ? (I[e3 + k] = w2(90 + t3, h), I[M + k] = w2(100 + t3, b)) : t3 = 60, I[e3] = w2(30 + t3, h), I[M] = w2(40 + t3, b);
|
|
104
|
+
}), s2.extend(I);
|
|
105
|
+
};
|
|
106
|
+
module.exports = w, w.default = w;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// node_modules/ansis/index.mjs
|
|
110
|
+
var import__ = __toESM(require_ansis(), 1);
|
|
111
|
+
var ansis_default = import__.default;
|
|
112
|
+
|
|
113
|
+
// src/utils.ts
|
|
114
|
+
async function exit_gracefully(server) {
|
|
115
|
+
if (process.stdin.isTTY) {
|
|
116
|
+
process.stdin.setRawMode(true);
|
|
117
|
+
process.stdin.resume();
|
|
118
|
+
process.stdin.setEncoding("utf8");
|
|
119
|
+
process.stdin.on("data", async (key) => {
|
|
120
|
+
if (key.toLowerCase() == "q") {
|
|
121
|
+
await server.stop();
|
|
122
|
+
if (process.stdin.isTTY) {
|
|
123
|
+
process.stdin.setRawMode(false);
|
|
124
|
+
process.stdin.pause();
|
|
125
|
+
}
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/main.html
|
|
133
|
+
var main_default = `<!DOCTYPE html>
|
|
134
|
+
<html lang="en">
|
|
135
|
+
<head>
|
|
136
|
+
<meta charset="UTF-8">
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
138
|
+
<title>tapmach</title>
|
|
139
|
+
<script>
|
|
140
|
+
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<style>
|
|
144
|
+
/* Root Colors */
|
|
145
|
+
:root {
|
|
146
|
+
--bg: #24273a;
|
|
147
|
+
--cell-bg: #363a4f;
|
|
148
|
+
--text: #cad3f5;
|
|
149
|
+
--size: 40px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Body */
|
|
153
|
+
body {
|
|
154
|
+
background: var(--bg);
|
|
155
|
+
color: var(--text);
|
|
156
|
+
font-family: 'JetBrains Mono', monospace;
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: flex-start;
|
|
161
|
+
height: 100vh;
|
|
162
|
+
margin: 0;
|
|
163
|
+
padding: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Textarea Styles */
|
|
167
|
+
#textarea {
|
|
168
|
+
width: 100%;
|
|
169
|
+
position: absolute;
|
|
170
|
+
box-sizing: border-box;
|
|
171
|
+
bottom: 0px;
|
|
172
|
+
left: 0px;
|
|
173
|
+
padding: 5px;
|
|
174
|
+
margin: 0;
|
|
175
|
+
}
|
|
176
|
+
textarea {
|
|
177
|
+
display: block;
|
|
178
|
+
box-sizing: border-box;
|
|
179
|
+
width: 100%;
|
|
180
|
+
|
|
181
|
+
line-height: 1.6rem;
|
|
182
|
+
letter-spacing: 0.5%;
|
|
183
|
+
|
|
184
|
+
background: #121212;
|
|
185
|
+
color: #e0e0e0;
|
|
186
|
+
border: 1px solid #2a2a2a;
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
padding: 10px;
|
|
189
|
+
|
|
190
|
+
outline: none;
|
|
191
|
+
resize: none;
|
|
192
|
+
|
|
193
|
+
font-size: 14pt;
|
|
194
|
+
font-family: "JetBrains Mono", monospace;
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
textarea:focus {
|
|
198
|
+
border-color: #444;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Run Button */
|
|
202
|
+
.run-btn {
|
|
203
|
+
background-color: var(--cell-bg);
|
|
204
|
+
color: var(--text);
|
|
205
|
+
|
|
206
|
+
border: 1px solid rgba(202, 211, 245, 0.1);
|
|
207
|
+
border-radius: 6px;
|
|
208
|
+
padding: 8px 10px;
|
|
209
|
+
font-size: 14px;
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
transition: all 0.2s ease;
|
|
213
|
+
outline: none;
|
|
214
|
+
|
|
215
|
+
display: inline-flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
transform-origin: center;
|
|
220
|
+
|
|
221
|
+
position: absolute;
|
|
222
|
+
top: 5px;
|
|
223
|
+
right: 5px;
|
|
224
|
+
}
|
|
225
|
+
.run-btn:hover {
|
|
226
|
+
background-color: #494d64;
|
|
227
|
+
border-color: var(--text);
|
|
228
|
+
}
|
|
229
|
+
.run-btn:active {
|
|
230
|
+
transform: scale(1.2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Speed Control */
|
|
234
|
+
.speed-control {
|
|
235
|
+
position: absolute;
|
|
236
|
+
top: 12px;
|
|
237
|
+
right: 60px;
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 8px;
|
|
241
|
+
font-size: 12px;
|
|
242
|
+
color: var(--text);
|
|
243
|
+
}
|
|
244
|
+
.speed-control input {
|
|
245
|
+
background: var(--cell-bg);
|
|
246
|
+
color: var(--text);
|
|
247
|
+
border: 1px solid rgba(202, 211, 245, 0.1);
|
|
248
|
+
border-radius: 4px;
|
|
249
|
+
padding: 4px;
|
|
250
|
+
width: 60px;
|
|
251
|
+
font-family: 'JetBrains Mono', monospace;
|
|
252
|
+
outline: none;
|
|
253
|
+
}
|
|
254
|
+
input::-webkit-outer-spin-button,
|
|
255
|
+
input::-webkit-inner-spin-button {
|
|
256
|
+
-webkit-appearance: none;
|
|
257
|
+
margin: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Scrolling Container */
|
|
261
|
+
#viewport {
|
|
262
|
+
width: 98vw;
|
|
263
|
+
max-height: 45vh;
|
|
264
|
+
height: 45vh;
|
|
265
|
+
margin-top: 8vh;
|
|
266
|
+
overflow-y: auto;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
padding: 10px;
|
|
269
|
+
scroll-behavior: smooth;
|
|
270
|
+
background: rgba(0, 0, 0, 0.2);
|
|
271
|
+
border-radius: 4px;
|
|
272
|
+
}
|
|
273
|
+
#tape {
|
|
274
|
+
display: grid;
|
|
275
|
+
grid-template-columns: repeat(auto-fill, var(--size));
|
|
276
|
+
gap: 4px;
|
|
277
|
+
justify-content: center;
|
|
278
|
+
padding-left: 0;
|
|
279
|
+
padding-right: 0;
|
|
280
|
+
gap: 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Cell Styling */
|
|
284
|
+
.cell {
|
|
285
|
+
width: var(--size);
|
|
286
|
+
height: var(--size);
|
|
287
|
+
background: var(--cell-bg);
|
|
288
|
+
border: 1px solid var(--bg);
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
font-size: 14px;
|
|
293
|
+
box-sizing: border-box;
|
|
294
|
+
flex-shrink: 0;
|
|
295
|
+
}
|
|
296
|
+
.cell.active {
|
|
297
|
+
border: 1px solid #a6da95;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Animations */
|
|
301
|
+
@keyframes create {
|
|
302
|
+
0% { transform: scale(0); opacity: 0; background: var(--cell-bg); }
|
|
303
|
+
100% { transform: scale(1); opacity: 1; background: var(--cell-bg); }
|
|
304
|
+
}
|
|
305
|
+
.animate-create {
|
|
306
|
+
animation: create 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@keyframes update {
|
|
310
|
+
0% { box-shadow: none; transform: scale(1); }
|
|
311
|
+
50% { box-shadow: none; transform: scale(1.2); z-index: 10; background: #494d64; }
|
|
312
|
+
100% { box-shadow: none; transform: scale(1); }
|
|
313
|
+
}
|
|
314
|
+
.animate-update { animation: update 0.5s ease-out; }
|
|
315
|
+
|
|
316
|
+
@keyframes destroy {
|
|
317
|
+
0% { transform: scale(1); opacity: 1; }
|
|
318
|
+
100% { transform: scale(0); opacity: 0; width: 0; }
|
|
319
|
+
}
|
|
320
|
+
.animate-destroy { animation: destroy 0.3s forwards;}
|
|
321
|
+
|
|
322
|
+
/* Styling Of Scrollbars */
|
|
323
|
+
#viewport::-webkit-scrollbar {
|
|
324
|
+
width: 3px;
|
|
325
|
+
}
|
|
326
|
+
#viewport::-webkit-scrollbar-track {
|
|
327
|
+
background: rgba(0, 0, 0, 0.2);
|
|
328
|
+
}
|
|
329
|
+
#viewport::-webkit-scrollbar-thumb {
|
|
330
|
+
background: #494d64;
|
|
331
|
+
border-radius: 6px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
textarea::-webkit-scrollbar {
|
|
335
|
+
width: 3px;
|
|
336
|
+
}
|
|
337
|
+
textarea::-webkit-scrollbar-track {
|
|
338
|
+
background-color: #121212;
|
|
339
|
+
border-radius: 6px;
|
|
340
|
+
}
|
|
341
|
+
textarea::-webkit-scrollbar-thumb {
|
|
342
|
+
background-color: #494d64;
|
|
343
|
+
border-radius: 6px;
|
|
344
|
+
}
|
|
345
|
+
</style>
|
|
346
|
+
</head>
|
|
347
|
+
<body>
|
|
348
|
+
<div class="speed-control">
|
|
349
|
+
<label for="speed">Interval (ms):</label>
|
|
350
|
+
<input type="number" id="speed" value="1000" min="0" step="100">
|
|
351
|
+
</div>
|
|
352
|
+
<button class="run-btn">
|
|
353
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
|
354
|
+
<path d="M8 5v14l11-7z"/>
|
|
355
|
+
</svg>
|
|
356
|
+
</button>
|
|
357
|
+
|
|
358
|
+
<div id="viewport">
|
|
359
|
+
<div id="tape"></div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div id="textarea">
|
|
363
|
+
<textarea name="main" id="main-textarea" rows="15" spellcheck="false" autocorrect="off" autofocus></textarea>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<script>
|
|
367
|
+
// parse and convert the text to a string
|
|
368
|
+
|
|
369
|
+
function parse(text) {
|
|
370
|
+
const lines = text.split(/\\r?\\n/).filter(str => str.trim() !== "");
|
|
371
|
+
let tokens = [];
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < lines.length; i++) {
|
|
374
|
+
let current_tokens = lines[i].split(/\\s+/);
|
|
375
|
+
|
|
376
|
+
if (current_tokens.includes('line_end')) {
|
|
377
|
+
throw Error("Invalid token in your code -> line_end");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
current_tokens.map((token) => {
|
|
381
|
+
tokens.push(token);
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
tokens.push('line_end')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return tokens;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function tokenToCode(code, currentToken, nextToken, speed) {
|
|
391
|
+
if (currentToken === 'create_cell') {
|
|
392
|
+
code.push('create_cell(');
|
|
393
|
+
} else if (currentToken === 'destroy_cell') {
|
|
394
|
+
code.push('destroy_cell(');
|
|
395
|
+
} else if (currentToken === 'add') {
|
|
396
|
+
code.push('add(')
|
|
397
|
+
} else if (currentToken === 'move') {
|
|
398
|
+
code.push('move(')
|
|
399
|
+
} else if (!isNaN(currentToken) && currentToken.trim() !== "") {
|
|
400
|
+
if (nextToken !== 'line_end') {
|
|
401
|
+
code.push(\`\${currentToken},\`)
|
|
402
|
+
} else {
|
|
403
|
+
code.push(\`\${currentToken}\`)
|
|
404
|
+
}
|
|
405
|
+
} else if (currentToken === 'line_end') {
|
|
406
|
+
code.push(\`); await sleep(\${speed});\`);
|
|
407
|
+
} else {
|
|
408
|
+
throw Error(\`Can't parse the code, wrong instruction: \${currentToken}\`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function convertToCode(text) {
|
|
413
|
+
let tokens = parse(text);
|
|
414
|
+
const speed = document.getElementById('speed').value || 1000;
|
|
415
|
+
let code = ['(async () => {'];
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
418
|
+
tokenToCode(code, tokens[i], tokens[i+1], speed);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
code.push('})();')
|
|
422
|
+
code = code.join('');
|
|
423
|
+
|
|
424
|
+
return code;
|
|
425
|
+
}
|
|
426
|
+
</script>
|
|
427
|
+
|
|
428
|
+
<script id="textarea-content-save">
|
|
429
|
+
const textarea = document.querySelector('textarea#main-textarea');
|
|
430
|
+
let lastSavedValue = localStorage.getItem('text-content') || '';
|
|
431
|
+
|
|
432
|
+
textarea.value = lastSavedValue;
|
|
433
|
+
|
|
434
|
+
function debounce(func, timeout = 500) {
|
|
435
|
+
let timer;
|
|
436
|
+
return (...args) => {
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const saveContent = debounce((value) => {
|
|
443
|
+
if (value === lastSavedValue) return;
|
|
444
|
+
localStorage.setItem('text-content', value);
|
|
445
|
+
lastSavedValue = value;
|
|
446
|
+
}, 1000);
|
|
447
|
+
|
|
448
|
+
textarea.addEventListener('input', (e) => saveContent(e.target.value))
|
|
449
|
+
</script>
|
|
450
|
+
<script>
|
|
451
|
+
const inputField = document.getElementById('speed');
|
|
452
|
+
|
|
453
|
+
window.addEventListener("load", function() {
|
|
454
|
+
inputField.value = localStorage.getItem('speed');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
inputField.addEventListener('input', (e) => {
|
|
459
|
+
localStorage.setItem('speed', e.target.value);
|
|
460
|
+
})
|
|
461
|
+
</script>
|
|
462
|
+
|
|
463
|
+
<script>
|
|
464
|
+
const tape = document.getElementById('tape');
|
|
465
|
+
const viewport = document.getElementById('viewport');
|
|
466
|
+
let cells = [];
|
|
467
|
+
let current_cell = 0;
|
|
468
|
+
let list = [];
|
|
469
|
+
|
|
470
|
+
async function update() {
|
|
471
|
+
if (cells.length === 0) return;
|
|
472
|
+
|
|
473
|
+
const target = cells[current_cell];
|
|
474
|
+
|
|
475
|
+
target.textContent = list[current_cell];
|
|
476
|
+
|
|
477
|
+
target.classList.remove('animate-update');
|
|
478
|
+
void target.offsetWidth;
|
|
479
|
+
|
|
480
|
+
target.classList.add('animate-update')
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function create_cell() {
|
|
484
|
+
const val = 0;
|
|
485
|
+
const div = document.createElement('div');
|
|
486
|
+
div.classList.add('cell', 'animate-create', \`i\${list.length+1}\`);
|
|
487
|
+
div.textContent = val;
|
|
488
|
+
|
|
489
|
+
list.push(val);
|
|
490
|
+
tape.appendChild(div);
|
|
491
|
+
cells.push(div);
|
|
492
|
+
if (list.length === 1) cells[current_cell].classList.add('active');
|
|
493
|
+
|
|
494
|
+
viewport.scrollTo({
|
|
495
|
+
left: viewport.scrollHeight,
|
|
496
|
+
behavior: 'smooth'
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function add(number) {
|
|
501
|
+
list[current_cell] += number;
|
|
502
|
+
|
|
503
|
+
update();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function destroy_cell() {
|
|
507
|
+
const target = cells[current_cell];
|
|
508
|
+
if (typeof target === null) return;
|
|
509
|
+
|
|
510
|
+
target.classList.add('animate-destroy');
|
|
511
|
+
|
|
512
|
+
setTimeout(() => target.remove(), 300);
|
|
513
|
+
move(-1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function move(index) {
|
|
517
|
+
if (current_cell+index < 0 || current_cell+index >= cells.length) return;
|
|
518
|
+
|
|
519
|
+
previous_cell = current_cell;
|
|
520
|
+
current_cell += index;
|
|
521
|
+
const target = cells[current_cell];
|
|
522
|
+
|
|
523
|
+
const rect = target.getBoundingClientRect();
|
|
524
|
+
const viewportRect = viewport.getBoundingClientRect();
|
|
525
|
+
|
|
526
|
+
if (rect.left >= viewportRect.left && rect.right <= viewportRect.right) {
|
|
527
|
+
cells[previous_cell].classList.remove('active');
|
|
528
|
+
cells[current_cell].classList.add('active');
|
|
529
|
+
} else {
|
|
530
|
+
target.scrollIntoView({
|
|
531
|
+
behavior: 'smooth',
|
|
532
|
+
block: 'center',
|
|
533
|
+
inline: 'nearest',
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
cells[previous_cell].classList.remove('active');
|
|
538
|
+
cells[current_cell].classList.add('active');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
</script>
|
|
542
|
+
|
|
543
|
+
<script>
|
|
544
|
+
const button = document.querySelector('.run-btn');
|
|
545
|
+
button.addEventListener('click', async (e) => {
|
|
546
|
+
let text = localStorage.getItem('text-content');
|
|
547
|
+
eval(convertToCode(text));
|
|
548
|
+
})
|
|
549
|
+
</script>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|
|
552
|
+
`;
|
|
553
|
+
|
|
554
|
+
// src/index.ts
|
|
555
|
+
console.clear();
|
|
556
|
+
var server = Bun.serve({
|
|
557
|
+
routes: {
|
|
558
|
+
"/": new Response(main_default, {
|
|
559
|
+
headers: { "Content-Type": "text/html" }
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
console.log(`Server running at ${ansis_default.blue.underline(server.url)}`);
|
|
564
|
+
console.log(`Press q to exit...`);
|
|
565
|
+
await exit_gracefully(server);
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tapmach",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"module": "./src/index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "bun run ./src/index.ts",
|
|
13
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "latest"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ansis": "^4.2.0"
|
|
23
|
+
}
|
|
24
|
+
}
|