Playground enhancements

This commit is contained in:
2025-07-15 19:58:58 -04:00
parent 3289c95e6a
commit bb2ae861b3
73 changed files with 834 additions and 451 deletions

View File

@@ -2,8 +2,11 @@
## Todo items for the playground ## Todo items for the playground
- [x] sample files - [x] sample files
- [x] codemirror migration
- [ ] make sample files available for import - [ ] make sample files available for import
- workaround is to visit the file first - workaround is to visit the file first
- we can put them in the zip file and pull them over the IPC
- [ ] make phone layout automatic
- [x] move newt to a worker (shim + newt + listener) - [x] move newt to a worker (shim + newt + listener)
- [x] tabs for source, compiler output - [x] tabs for source, compiler output
- [x] Show errors in editor - [x] Show errors in editor
@@ -13,3 +16,7 @@
- [x] publish / host on github - [x] publish / host on github
- [ ] multiple persistent files - [ ] multiple persistent files
- [x] kill return for autocomplete - [x] kill return for autocomplete
- [x] save to url (copy from idris2-playground)
- [ ] click on url
- [ ] settings

View File

@@ -1 +0,0 @@
../../../aoc2023/Aoc.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day1.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day10.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day11.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day11b.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day12.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day13.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day14.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day15.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day16.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day17.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day18.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day19.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day2.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day20.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day21.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day22.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day23.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day24.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day25.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day3.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day4.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day5.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day6.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day7.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day8.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Day9.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/DayXX.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Node.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Parser.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/Prelude.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/SortedMap.newt

View File

@@ -1 +0,0 @@
../../../aoc2024/day1

View File

@@ -1 +0,0 @@
../../../aoc2024/day10

View File

@@ -1 +0,0 @@
../../../aoc2024/day11

View File

@@ -1 +0,0 @@
../../../aoc2024/day12

View File

@@ -1 +0,0 @@
../../../aoc2024/day13

View File

@@ -1 +0,0 @@
../../../aoc2024/day14

View File

@@ -1 +0,0 @@
../../../aoc2024/day15

View File

@@ -1 +0,0 @@
../../../aoc2024/day16

View File

@@ -1 +0,0 @@
../../../aoc2024/day17

View File

@@ -1 +0,0 @@
../../../aoc2024/day18

View File

@@ -1 +0,0 @@
../../../aoc2024/day19

View File

@@ -1 +0,0 @@
../../../aoc2024/day2

View File

@@ -1 +0,0 @@
../../../aoc2024/day20

View File

@@ -1 +0,0 @@
../../../aoc2024/day21

View File

@@ -1 +0,0 @@
../../../aoc2024/day22

View File

@@ -1 +0,0 @@
../../../aoc2024/day23

View File

@@ -1 +0,0 @@
../../../aoc2024/day24

View File

@@ -1 +0,0 @@
../../../aoc2024/day25

View File

@@ -1 +0,0 @@
../../../aoc2024/day3

View File

@@ -1 +0,0 @@
../../../aoc2024/day4

View File

@@ -1 +0,0 @@
../../../aoc2024/day5

View File

@@ -1 +0,0 @@
../../../aoc2024/day6

View File

@@ -1 +0,0 @@
../../../aoc2024/day7

View File

@@ -1 +0,0 @@
../../../aoc2024/day8

View File

@@ -1 +0,0 @@
../../../aoc2024/day9

View File

@@ -1 +0,0 @@
../../src

View File

@@ -0,0 +1,14 @@
import test from "node:test";
import assert from "node:assert";
import { b64decode, b64encode } from "./base64.ts";
test("round trip", () => {
for (let s of ["", "a", "aa", "aaa", "aaaa", "aaaaa", "aaaaaa"]) {
let t = new TextEncoder().encode(s);
console.log(t, t + "");
let enc = b64encode(t);
assert.equal(enc.length, Math.ceil((t.length * 8) / 6));
assert.equal(b64decode(b64encode(t)) + "", t + "");
console.log("---");
}
});

45
playground/src/base64.ts Normal file
View File

@@ -0,0 +1,45 @@
// tables
const i2c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const c2i: Record<string, number> = {};
i2c.split("").forEach((c, i) => (c2i[c] = i));
export function b64encode(data: Uint8Array): string {
let rval = "";
let i = 0;
while (i < data.length) {
let v = data[i++];
// aaaaaa aa
rval += i2c[v >> 2];
// aabbbb bbbb
v = ((v & 3) << 8) | data[i++];
rval += i2c[v >> 4];
if (i > data.length) break;
// bbbbcc cccccc
v = ((v & 15) << 8) | data[i++];
rval += i2c[v >> 6];
if (i > data.length) break;
// cccccc
rval += i2c[v & 63];
}
return rval;
}
export function b64decode(s: string) {
let i = 0;
let arr: number[] = [];
while (i < s.length) {
// aaaaaabb bbbb
let acc = (c2i[s[i++]] << 6) | c2i[s[i++]];
arr.push(acc >> 4);
if (i >= s.length) break;
// bbbbcccc cc
acc = ((acc & 15) << 6) | c2i[s[i++]];
arr.push(acc >> 2);
if (i >= s.length) break;
// ccdddddd
acc = ((acc & 3) << 6) | c2i[s[i++]];
arr.push(acc);
if (i >= s.length) break;
}
return Uint8Array.from(arr);
}

View File

@@ -81,7 +81,6 @@ function tokenizer(stream: StringStream, state: State): string | null {
let word = stream.current(); let word = stream.current();
if (keywords.includes(word)) return "keyword"; if (keywords.includes(word)) return "keyword";
if (word[0] >= "A" && word[0] <= "Z") return "typename"; if (word[0] >= "A" && word[0] <= "Z") return "typename";
console.log('IDENT', )
return "identifier"; return "identifier";
} }
// unhandled // unhandled
@@ -136,7 +135,7 @@ export class CMEditor implements AbstractEditor {
// For indent on return // For indent on return
indentService.of((ctx, pos) => { indentService.of((ctx, pos) => {
let line = ctx.lineAt(pos) let line = ctx.lineAt(pos)
if (!line) return null if (!line || !line.from) return null
let prevLine = ctx.lineAt(line.from - 1); let prevLine = ctx.lineAt(line.from - 1);
if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) { if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) {
let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0 let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0

131
playground/src/deflate.ts Normal file
View File

@@ -0,0 +1,131 @@
// This is a minimal deflate implementation.
//
// It writes data that zlib can decompress, but sticks to the built-in huffman tables
// and a simple search heuristic to keep the code size down.
//
// TODO
// - initialize offsets to something other than zero (MAXINT32)
// - offsets are all wrong.
const maxMatchOffset = 1 << 14
const tableSize = 1 << 14
const lengthExtra = [0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0];
const distExtra = [0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13];
const lengthCode : number[] = []
const lengthBase : number[] = []
const distCode : number[] = [];
const distBase : number[] = [];
(function() {
// see trees.c
let code
let length = 0
for (code = 0; code < 28; code++) {
lengthBase[code] = length;
for (let n = 0; n < (1<<lengthExtra[code]); n++) {
lengthCode[length++] = code
}
}
lengthCode[length-1] = code
// Initialize the mapping dist (0..32K) -> dist code (0..29)
let dist = 0;
for (code = 0 ; code < 16; code++) {
distBase[code] = dist;
for (let n = 0; n < (1<<distExtra[code]); n++) {
distCode[dist++] = code;
}
}
dist >>= 7; // from now on, all distances are divided by 128
for ( ; code < 30; code++) {
distBase[code] = dist << 7;
for (let n = 0; n < (1<<(distExtra[code]-7)); n++) {
distCode[256 + dist] = code;
dist++
}
}
})()
export function deflate(buf: Uint8Array, raw = true) {
let bitpos = 0
let out = new Uint8Array(65536)
function write(bits: number,value:number,backwards?: boolean) {
for (let i = 0;i < bits;i++) {
const bytepos = bitpos >> 3
const mask = 1 << (bitpos&7)
if (bytepos + 10 > out.length) {
const tmp = new Uint8Array(out.length*1.5)
tmp.set(out)
out = tmp
}
let j = backwards ? i : bits - i - 1
if (value & (1 << j)) out[bytepos] |= mask
bitpos++
}
}
function emit(value: number) {
if (value < 144) write(8, 48+value)
else if (value < 256) write(9, value-144+400)
else if (value < 280) write(7, value-256)
else write(8, value-280+192)
}
if (!raw) {
write(8,0x78,true)
write(8,0x9c,true)
}
write(1,1)
write(2,2)
const src = new DataView(buf.buffer)
const hash = (u:number) => (u * 0x1e35a7bd) >>> 18 // top 14 bits
const values = new Int32Array(tableSize)
const offsets = new Int32Array(tableSize)
const sLimit = buf.length - 3
let s = 0
let nextEmit = 0
while ( s < sLimit) {
let cur_val = src.getUint32(s, true)
let cur_hash = hash(cur_val)
const cand_val = values[cur_hash]
const cand_off = offsets[cur_hash]
offsets[cur_hash] = s
values[cur_hash] = cur_val
let offset = s - cand_off
if (0 < offset && offset < maxMatchOffset && cur_val == cand_val) {
for (;nextEmit<s;nextEmit++) emit(buf[nextEmit])
s += 4
let t = cand_off + 4
let l = 0
while (s+l < buf.length && buf[s+l] === buf[t+l] && l<255) l++
const l_code = lengthCode[l+1]
emit(l_code+257)
write(lengthExtra[l_code],l + 1 - lengthBase[l_code],true)
const d_code = (offset<257) ? distCode[offset-1] : distCode[256+((offset-1)>>7)]
write(5,d_code)
write(distExtra[d_code],offset-1-distBase[d_code],true)
s += l
nextEmit = s
} else {
s++
}
}
for (;nextEmit<buf.length;nextEmit++) emit(buf[nextEmit])
emit(256)
let adler = 1
let s2 = 0
for (let i=0;i<buf.length;i++) {
adler = (adler + buf[i] % 65521)
s2 = (s2 + adler) % 65521
}
adler |= (s2 << 16)
let len = 1 + (bitpos >> 3)
if (raw) return out.slice(0,len)
new DataView(out.buffer).setInt32(len,adler,false)
return out.slice(0, len+4)
}

View File

@@ -9,7 +9,14 @@ export interface Handle {
interface Process { interface Process {
argv: string[]; argv: string[];
platform: string;
exit(_: number): void; exit(_: number): void;
stdout: {
write(s: string): unknown
};
cwd(): string;
env: Record<string,string>
__lasterr: {errno: number}
} }
export interface NodeShim { export interface NodeShim {
stdout: string; stdout: string;

View File

@@ -62,7 +62,7 @@ window.addEventListener("message", (ev: MessageEvent<Message>) => {
let { src } = ev.data; let { src } = ev.data;
try { try {
sendMessage({ type: "setConsole", messages: [] }); sendMessage({ type: "setConsole", messages: [] });
eval(src); (new Function(src))();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }

View File

@@ -0,0 +1,16 @@
import test from "node:test";
import assert from "node:assert";
import { readFileSync } from "node:fs";
import { deflate } from "./deflate.ts";
import { inflate } from "./inflate.ts";
import { b64encode } from "./base64.ts";
test('round trip', ()=>{
let src = readFileSync('src/inflate.ts','utf8')
let smol = deflate(new TextEncoder().encode(src))
let big = inflate(smol)
assert.equal(src, new TextDecoder().decode(big))
console.log(src.length, smol.length, b64encode(smol).length)
})

304
playground/src/inflate.ts Normal file
View File

@@ -0,0 +1,304 @@
// I wrote this inflate years ago, seems to work for zip
class BitReader {
pos = 0;
bits = 0;
acc = 0;
len: number;
data: Uint8Array;
constructor(data: Uint8Array) {
this.data = data;
this.len = data.length;
}
read(bits: number) {
while (this.bits < bits) {
if (this.pos >= this.len) throw "EOF";
this.acc |= this.data[this.pos++] << this.bits;
this.bits += 8;
}
let rval = this.acc & ((1 << bits) - 1);
this.acc >>= bits;
this.bits -= bits;
return rval;
}
read8() {
if (this.pos > this.len) throw "EOF";
// flush
if (this.bits > 0) {
this.bits = 0;
this.acc = 0;
}
return this.data[this.pos++];
}
read16() {
let rval = this.read8() * 256 + this.read8();
return rval;
}
}
class HuffDic {
limit: number[];
codes: number[];
base: number[];
constructor(lengths: number[]) {
let counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let min = 0;
let max = 0;
for (let i = 0; i < lengths.length; i++) {
let len = lengths[i];
if (len != 0) {
if (len < min || min == 0) min = len;
if (len > max) max = len;
counts[len]++;
}
}
this.base = [];
this.limit = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
];
let code = 0;
let seq = 0;
let next_code = [];
for (let i = min; i <= max; i++) {
let n = counts[i];
next_code[i] = code;
this.base[i] = code - seq;
code += n;
seq += n;
this.limit[i] = code - 1;
code <<= 1;
}
this.codes = [];
for (let i = 0; i < lengths.length; i++) {
let n = lengths[i];
if (n != 0) {
code = next_code[n];
next_code[n]++;
if (!this.base[n]) this.base[n] = 0;
seq = code - this.base[n];
this.codes[seq] = i;
}
}
}
readSymbol(r: BitReader) {
let v = 0;
for (let i = 1; i < this.limit.length; i++) {
v <<= 1;
v |= r.read(1);
let limit = this.limit[i];
if (v <= limit) {
return this.codes[v - this.base[i]];
}
}
throw "eHUFF";
}
}
let staticHuff: HuffDic;
let distHuff: HuffDic;
{
let tmp = [];
for (let i = 0; i < 144; i++) tmp[i] = 8;
for (let i = 144; i < 256; i++) tmp[i] = 9;
for (let i = 256; i < 280; i++) tmp[i] = 7;
for (let i = 280; i < 288; i++) tmp[i] = 8;
staticHuff = new HuffDic(tmp);
tmp = [];
for (let i = 0; i < 30; i++) {
tmp[i] = 5;
}
distHuff = new HuffDic(tmp);
}
export function inflate(input: Uint8Array) {
let r = new BitReader(input);
let out = new Uint8Array(65536);
let pos = 0;
const push = (b: number) => {
if (pos + 10 > out.length) {
const tmp = new Uint8Array(out.length * 1.5);
tmp.set(out);
out = tmp;
}
out[pos++] = b;
};
let fin = 0;
while (!fin) {
fin = r.read(1);
let btype = r.read(2);
let huff2;
let huff3;
if (btype == 0) {
let len = r.read16();
let nlen = r.read16();
for (let i = 0; i < len; i++) push(r.read8());
} else if (btype == 1) {
// fixed huffman
huff2 = staticHuff;
huff3 = distHuff;
} else if (btype == 2) {
// dynamic huffman
let hlit = r.read(5) + 257;
let hdist = r.read(5) + 1;
let hclen = r.read(4) + 4;
let lengths: number[] = [];
for (let i = 0; i < 19; i++) lengths[i] = 0;
let xx = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
for (let i = 0; i < hclen; i++) {
let t = r.read(3);
lengths[xx[i]] = t;
}
let huff = new HuffDic(lengths);
lengths = [];
while (true) {
let k = huff.readSymbol(r);
if (k < 16) {
lengths.push(k);
} else if (k == 16) {
let count = r.read(2) + 3;
if (lengths.length == 0) throw new Error("no lengths?");
for (; count > 0; count--) lengths.push(lengths[lengths.length - 1]);
} else if (k == 17) {
let count = r.read(3) + 3;
for (; count > 0; count--) lengths.push(0);
} else if (k == 18) {
let count = r.read(7) + 11;
for (; count > 0; count--) lengths.push(0);
}
if (lengths.length >= hlit + hdist) break;
}
huff2 = new HuffDic(lengths.slice(0, hlit));
huff3 = new HuffDic(lengths.slice(hlit));
} else {
throw new Error("btype " + btype);
}
if (huff2) {
while (true) {
let k = huff2.readSymbol(r);
let len = 0;
let n = 0; // extra bits
if (k < 256) {
push(k);
continue;
} else if (k == 256) {
// End of block
break;
} else if (k < 265) {
len = k - 257 + 3;
n = 0;
} else if (k < 269) {
len = (k - 265) * 2 + 11;
n = 1;
} else if (k < 273) {
len = (k - 269) * 4 + 19;
n = 2;
} else if (k < 277) {
len = (k - 273) * 8 + 35;
n = 3;
} else if (k < 281) {
len = (k - 277) * 16 + 67;
n = 4;
} else if (k < 285) {
len = (k - 281) * 32 + 131;
n = 5;
} else {
len = 258;
n = 0;
}
if (n > 0) len += r.read(n);
// distance
if (r.pos > r.len) throw new Error("EOF");
let dist;
if (huff3) dist = huff3.readSymbol(r);
else dist = r.read(5);
if (dist < 4) {
dist++;
} else if (dist < 30) {
let db = (dist - 2) >> 1;
let extra = (dist & 1) << db;
extra |= r.read(db);
dist = (1 << (db + 1)) + 1 + extra;
} else {
throw new Error(`dist ${dist}`);
}
if (dist > pos) throw new Error(`dist ${dist} > pos ${pos}`);
let s: number = pos - dist;
for (let i = 0; i < len; i++) push(out[s + i]);
}
}
}
return out.slice(0, pos);
}
interface Entry {
size: number;
start: number;
end: number;
}
export class ZipFile {
data: Uint8Array;
entries: Record<string, Entry>;
constructor(data: Uint8Array) {
this.data = data;
this.entries = {};
let td = new TextDecoder();
let error = (msg: string) => {
throw new Error(`${msg} at ${pos}`);
};
let view = new DataView(data.buffer);
let pos = 0;
while (pos < view.byteLength) {
let sig = view.getUint32(pos, true);
if (sig == 0x02014b50) break;
if (sig != 0x04034b50) error(`bad sig ${sig.toString(16)}`);
let method = view.getUint16(pos + 8, true);
if (method !== 8) throw new Error(`method ${method} not handled`)
let csize = view.getUint32(pos + 18, true);
let size = view.getUint32(pos + 22, true);
let fnlen = view.getUint16(pos + 26, true);
let eflen = view.getUint16(pos + 28, true);
let fn = td.decode(data.slice(pos + 30, pos + 30 + fnlen));
if (size) {
let start = pos + 30 + fnlen + eflen;
let end = start + csize;
this.entries[fn] = { size, start, end };
}
pos = pos + 30 + fnlen + eflen + csize;
}
}
getData(name: string) {
let { start, end, size } = this.entries[name];
return inflate(new Uint8Array(this.data.slice(start, end)));
}
}

74
playground/src/ipc.ts Normal file
View File

@@ -0,0 +1,74 @@
export type Result<A> =
| { status: "ok"; value: A }
| { status: "err"; error: string };
export interface API {
save(fileName: string, content: string): string;
typeCheck(fileName: string): string;
compile(fileName: string): string;
}
export interface Message<K extends keyof API> {
id: number;
key: K;
args: Parameters<API[K]>;
}
export interface ResponseMSG {
id: number;
result: string;
}
type Suspense = {
resolve: (value: any | PromiseLike<any>) => void;
reject: (reason?: any) => void;
};
export class IPC {
callbacks: Record<number, Suspense> = {};
_postMessage: <K extends keyof API>(msg: Message<K>) => void;
lastID = 1;
constructor() {
const newtWorker = new Worker("worker.js");
this._postMessage = <K extends keyof API>(msg: Message<K>) =>
newtWorker.postMessage(msg);
// Safari/MobileSafari have small stacks in webworkers.
if (navigator.vendor.includes("Apple")) {
const workerFrame = document.createElement("iframe");
workerFrame.src = "worker.html";
workerFrame.style.display = "none";
document.body.appendChild(workerFrame);
this._postMessage = (msg: any) =>
workerFrame.contentWindow?.postMessage(msg, "*");
window.addEventListener("message", (ev) => this.onmessage(ev));
} else {
newtWorker.onmessage = this.onmessage
}
// Need to handle messages from the other iframe too? Or at least ignore them.
}
onmessage = (ev: MessageEvent<ResponseMSG>) => {
console.log("GET", ev.data);
// Maybe key off of type
if (ev.data.id) {
let suspense = this.callbacks[ev.data.id];
if (suspense) {
suspense.resolve(ev.data.result);
delete this.callbacks[ev.data.id];
}
console.log("result", ev.data, "suspense", suspense);
}
}
async sendMessage<K extends keyof API>(
key: K,
args: Parameters<API[K]>
): Promise<ReturnType<API[K]>> {
return new Promise((resolve, reject) => {
let id = this.lastID++;
this.callbacks[id] = { resolve, reject };
console.log("POST", { id, key, args });
this._postMessage({ id, key, args });
});
}
}
class IPCClient {}

View File

@@ -1,32 +1,59 @@
import { effect, signal } from "@preact/signals"; import { effect, signal } from "@preact/signals";
import { Diagnostic } from "@codemirror/lint"; import { Diagnostic } from "@codemirror/lint";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { h, render } from "preact"; import { h, render, VNode } from "preact";
import { ChangeEvent } from "preact/compat"; import { ChangeEvent } from "preact/compat";
import { archive, preload } from "./preload.ts"; import { archive, preload } from "./preload.ts";
import { b64decode, b64encode } from "./base64";
import { import {
AbstractEditor, AbstractEditor,
EditorDelegate, EditorDelegate,
CompileReq,
CompileRes,
Message,
TopData, TopData,
Marker, Marker,
} from "./types.ts"; } from "./types.ts";
import { CMEditor } from "./cmeditor.ts"; import { CMEditor } from "./cmeditor.ts";
import { deflate } from "./deflate.ts";
import { inflate } from "./inflate.ts";
import { IPC } from "./ipc.ts";
let topData: undefined | TopData; let topData: undefined | TopData;
const newtWorker = new Worker("worker.js"); const ipc = new IPC();
// :FIXME use because Safari
let postMessage = (msg: CompileReq) => newtWorker.postMessage(msg); function mdline2nodes(s: string) {
// Safari/MobileSafari have small stacks in webworkers. let cs: (VNode|string)[] = []
if (navigator.vendor.includes("Apple")) { let toks = s.matchAll(/(\*\*.*?\*\*)|(\*.*?\*)|(_.*?_)|[^*]+|\*/g)
const workerFrame = document.createElement("iframe"); for (let tok of toks) {
workerFrame.src = "worker.html"; if (tok[1]) cs.push(h('b',{},tok[0].slice(2,-2)))
workerFrame.style.display = "none"; else if (tok[2]) cs.push(h('em',{},tok[0].slice(1,-1)))
document.body.appendChild(workerFrame); else if (tok[3]) cs.push(h('em',{},tok[0].slice(1,-1)))
postMessage = (msg: any) => workerFrame.contentWindow?.postMessage(msg, "*"); else cs.push(tok[0])
}
return cs
}
function md2nodes(md: string) {
let rval: VNode[] = []
let list: VNode[] | undefined
for (let line of md.split("\n")) {
if (line.startsWith('- ')) {
list = list ?? []
list.push(h('li', {}, mdline2nodes(line.slice(2))))
continue
}
if (list) {
rval.push(h('ul', {}, list))
list = undefined
}
if (line.startsWith('# ')) {
rval.push(h('h2', {}, mdline2nodes(line.slice(2))))
} else if (line.startsWith('## ')) {
rval.push(h('h3', {}, mdline2nodes(line.slice(3))))
} else {
rval.push(h('div', {}, mdline2nodes(line)))
}
}
return rval
} }
// iframe for running newt output // iframe for running newt output
@@ -35,13 +62,20 @@ iframe.src = "frame.html";
iframe.style.display = "none"; iframe.style.display = "none";
document.body.appendChild(iframe); document.body.appendChild(iframe);
function run(src: string) { async function refreshJS() {
if (!state.javascript.value) {
let src = state.editor.value!.getValue();
console.log("SEND TO", iframe.contentWindow); console.log("SEND TO", iframe.contentWindow);
const fileName = state.currentFile.value; const fileName = state.currentFile.value;
// postMessage({ type: "compileRequest", fileName, src }); // maybe send fileName, src?
await ipc.sendMessage("save", [fileName, src]);
let js = await ipc.sendMessage("compile", [fileName]);
state.javascript.value = js;
}
} }
function runOutput() { async function runOutput() {
await refreshJS()
const src = state.javascript.value; const src = state.javascript.value;
console.log("RUN", iframe.contentWindow); console.log("RUN", iframe.contentWindow);
try { try {
@@ -68,55 +102,47 @@ function setOutput(output: string) {
state.output.value = output; state.output.value = output;
} }
let lastID = 0; window.addEventListener("message", (ev) => {
const nextID = () => "" + lastID++; if (ev.data.id) return;
window.onmessage = (ev: MessageEvent<Message>) => {
console.log("window got", ev.data); console.log("window got", ev.data);
if ("messages" in ev.data) state.messages.value = ev.data.messages; if ("messages" in ev.data) state.messages.value = ev.data.messages;
if ("message" in ev.data) { if ("message" in ev.data) {
state.messages.value = [...state.messages.value, ev.data.message]; state.messages.value = [...state.messages.value, ev.data.message];
} }
// safari callback });
if ("output" in ev.data) {
newtWorker.onmessage?.(ev) async function copyToClipboard(ev: Event) {
setOutput(ev.data.output); ev.preventDefault();
state.javascript.value = ev.data.javascript; let src = state.editor.value!.getValue();
let hash = `#code/${b64encode(deflate(new TextEncoder().encode(src)))}`;
window.location.hash = hash;
await navigator.clipboard.writeText(window.location.href);
state.toast.value = "URL copied to clipboard";
setTimeout(() => (state.toast.value = ""), 2_000);
} }
};
// TODO wrap up IPC // We could push this into the editor
document.addEventListener("keydown", (ev) => {
if ((ev.metaKey || ev.ctrlKey) && ev.code == "KeyS")
copyToClipboard(ev);
});
type Suspense<T> = { function getSavedCode() {
resolve: (value: T | PromiseLike<T>) => void; let value: string = localStorage.idrisCode || LOADING;
reject: (reason?: any) => void; let hash = window.location.hash;
}; if (hash.startsWith("#code/")) {
try {
const callbacks: Record<string, Suspense<string>> = {}; value = new TextDecoder().decode(inflate(b64decode(hash.slice(6))));
} catch (e) {
newtWorker.onmessage = (ev: MessageEvent<CompileRes>) => { console.error(e);
let suspense = callbacks[ev.data.id];
if (suspense) {
suspense.resolve(ev.data.output);
delete callbacks[ev.data.id];
} }
console.log('result', ev.data, 'suspense', suspense)
// FIXME - we want to have the callback take a response for its command
setOutput(ev.data.output);
state.javascript.value = ev.data.javascript;
};
function runCommand(req: CompileReq) {
return new Promise<string>(
(resolve, reject) => {
callbacks[req.id] = { resolve, reject }
postMessage(req);
} }
); return value;
} }
const state = { const state = {
output: signal(""), output: signal(""),
toast: signal(""),
javascript: signal(""), javascript: signal(""),
messages: signal<string[]>([]), messages: signal<string[]>([]),
editor: signal<AbstractEditor | null>(null), editor: signal<AbstractEditor | null>(null),
@@ -166,7 +192,7 @@ document.addEventListener("keydown", (ev) => {
const LOADING = "module Loading\n"; const LOADING = "module Loading\n";
let value = localStorage.code || LOADING; let value = getSavedCode() || LOADING;
let initialVertical = localStorage.vertical == "true"; let initialVertical = localStorage.vertical == "true";
interface EditorProps { interface EditorProps {
@@ -176,13 +202,8 @@ const language: EditorDelegate = {
getEntry(word, _row, _col) { getEntry(word, _row, _col) {
return topData?.context.find((entry) => entry.name === word); return topData?.context.find((entry) => entry.name === word);
}, },
onChange(value) { onChange(_value) {
// run via the linter now // we're using lint() now
// clearTimeout(timeout);
// timeout = setTimeout(() => {
// run(value);
// localStorage.code = value;
// }, 1000);
}, },
getFileName() { getFileName() {
if (!topData) return ""; if (!topData) return "";
@@ -192,39 +213,35 @@ const language: EditorDelegate = {
async lint(view) { async lint(view) {
console.log("LINT"); console.log("LINT");
let src = view.state.doc.toString(); let src = view.state.doc.toString();
localStorage.code = src localStorage.code = src;
let module = src.match(/module\s+([^\s]+)/)?.[1] let module = src.match(/module\s+([^\s]+)/)?.[1];
if (module) { if (module) {
// This causes problems with stuff like aoc/... that reference files in the same directory // This causes problems with stuff like aoc/...
// state.currentFile.value = module.replace('.','/')+'.newt' state.currentFile.value = module.replace(".", "/") + ".newt";
} }
state.javascript.value = ''
let fileName = state.currentFile.value; let fileName = state.currentFile.value;
console.log("FN", fileName); console.log("FN", fileName);
try { try {
let out = await runCommand({ await ipc.sendMessage("save", [fileName, src]);
id: nextID(), let out = await ipc.sendMessage("typeCheck", [fileName]);
type: "compileRequest", setOutput(out);
fileName,
src,
compile: false,
});
console.log("OUT", out);
let markers = processOutput(out); let markers = processOutput(out);
let diags: Diagnostic[] = [] let diags: Diagnostic[] = [];
for (let marker of markers) { for (let marker of markers) {
let col = marker.startColumn let col = marker.startColumn;
let line = view.state.doc.line(marker.startLineNumber) let line = view.state.doc.line(marker.startLineNumber);
const pos = line.from + col - 1; const pos = line.from + col - 1;
let word = view.state.wordAt(pos) let word = view.state.wordAt(pos);
diags.push({ diags.push({
from: word?.from ?? pos, from: word?.from ?? pos,
to: word?.to ?? pos + 1, to: word?.to ?? pos + 1,
severity: marker.severity, severity: marker.severity,
message: marker.message, message: marker.message,
}) });
} }
return diags return diags;
} catch (e) { } catch (e) {
console.log("ERR", e); console.log("ERR", e);
} }
@@ -243,7 +260,6 @@ function Editor({ initialValue }: EditorProps) {
state.editor.value = editor; state.editor.value = editor;
editor.setDark(state.dark.value); editor.setDark(state.dark.value);
if (initialValue === LOADING) loadFile("Tour.newt"); if (initialValue === LOADING) loadFile("Tour.newt");
else run(initialValue);
}, []); }, []);
return h("div", { id: "editor", ref }); return h("div", { id: "editor", ref });
@@ -260,6 +276,38 @@ function Result() {
return h("div", { id: "result" }, text); return h("div", { id: "result" }, text);
} }
function Help() {
return h("div", { id: "help" },
md2nodes(`
# Newt Playground
The editor will typecheck the file with newt and render errors as the file is changed. The current file is saved to localStorage and will be restored if there is no data in the URL. Cmd-s / Ctrl-s will create a url embedding the file contents. There is a layout toggle for phone use.
## Tabs
**Output** - Displays the compiler output, which is also used to render errors and info annotations in the editor.
**JS** - Displays the javascript translation of the file
**Console** - Displays the console output from running the javascript
**Help** - Displays this help file
## Buttons
▶ Compile and run the current file in an iframe, console output is collected to the console tab.
📋 Embed the current file in the URL and copy to clipboard
↕ or ↔ Toggle vertical or horziontal layout (for mobile)
## Keyboard
*C-s or M-s* - Embed the current file in the URL and copy to clipboard
`)
)
}
function Console() { function Console() {
const messages = state.messages.value ?? []; const messages = state.messages.value ?? [];
return h( return h(
@@ -272,6 +320,7 @@ function Console() {
const RESULTS = "Output"; const RESULTS = "Output";
const JAVASCRIPT = "JS"; const JAVASCRIPT = "JS";
const CONSOLE = "Console"; const CONSOLE = "Console";
const HELP = "Help";
function Tabs() { function Tabs() {
const [selected, setSelected] = useState(localStorage.tab ?? RESULTS); const [selected, setSelected] = useState(localStorage.tab ?? RESULTS);
@@ -289,6 +338,10 @@ function Tabs() {
if (state.messages.value.length) setSelected(CONSOLE); if (state.messages.value.length) setSelected(CONSOLE);
}, [state.messages.value]); }, [state.messages.value]);
useEffect(() => {
if (selected === JAVASCRIPT && !state.javascript.value) refreshJS();
}, [selected, state.javascript.value]);
let body; let body;
switch (selected) { switch (selected) {
case RESULTS: case RESULTS:
@@ -300,6 +353,9 @@ function Tabs() {
case CONSOLE: case CONSOLE:
body = h(Console, {}); body = h(Console, {});
break; break;
case HELP:
body = h(Help, {});
break;
default: default:
body = h("div", {}); body = h("div", {});
} }
@@ -312,7 +368,8 @@ function Tabs() {
{ className: "tabBar" }, { className: "tabBar" },
Tab(RESULTS), Tab(RESULTS),
Tab(JAVASCRIPT), Tab(JAVASCRIPT),
Tab(CONSOLE) Tab(CONSOLE),
Tab(HELP),
), ),
h("div", { className: "tabBody" }, body) h("div", { className: "tabBody" }, body)
); );
@@ -348,12 +405,15 @@ function EditWrap({
loadFile(fn); loadFile(fn);
} }
}; };
let d = vertical
? "M0 0 h20 v20 h-20 z M0 10 h20" const char = vertical ? "↕" : "↔"
: "M0 0 h20 v20 h-20 z M10 0 v20";
let play = "M0 0 L20 10 L0 20 z"; // let d = vertical
let svg = (d: string) => // ? "M0 0 h20 v20 h-20 z M0 10 h20"
h("svg", { width: 20, height: 20, className: "icon" }, h("path", { d })); // : "M0 0 h20 v20 h-20 z M10 0 v20";
// let play = "M0 0 L20 10 L0 20 z";
// let svg = (d: string) =>
// h("svg", { width: 20, height: 20, className: "icon" }, h("path", { d }));
return h( return h(
"div", "div",
{ className: "tabPanel left" }, { className: "tabPanel left" },
@@ -366,8 +426,10 @@ function EditWrap({
options options
), ),
h("div", { style: { flex: "1 1" } }), h("div", { style: { flex: "1 1" } }),
h("button", { onClick: runOutput }, svg(play)), h("button", { onClick: copyToClipboard, title: "copy url" }, "📋"),
h("button", { onClick: toggle }, svg(d)) h("button", { onClick: runOutput, title: "run program" }, "▶"),
h("button", { onClick: toggle, title: "change layout" }, char),
// h("button", { onClick: toggle }, svg(d))
), ),
h("div", { className: "tabBody" }, h(Editor, { initialValue: value })) h("div", { className: "tabBody" }, h(Editor, { initialValue: value }))
); );
@@ -379,10 +441,15 @@ function App() {
setVertical(!vertical); setVertical(!vertical);
localStorage.vertical = !vertical; localStorage.vertical = !vertical;
}; };
let toast;
if (state.toast.value) {
toast = h("p", { className: "toast" }, h("div", {}, state.toast.value));
}
let className = `wrapper ${vertical ? "vertical" : "horizontal"}`; let className = `wrapper ${vertical ? "vertical" : "horizontal"}`;
return h( return h(
"div", "div",
{ className }, { className },
toast,
h(EditWrap, { vertical, toggle }), h(EditWrap, { vertical, toggle }),
h(Tabs, {}) h(Tabs, {})
); );
@@ -390,16 +457,10 @@ function App() {
render(h(App, {}), document.getElementById("app")!); render(h(App, {}), document.getElementById("app")!);
let timeout: number | undefined;
// Adapted from the vscode extension, but types are slightly different
// and positions are 1-based.
const processOutput = ( const processOutput = (
// editor: AbstractEditor,
output: string output: string
) => { ) => {
// let model = editor.getModel()!; console.log("process output", output);
console.log('process output', output)
let markers: Marker[] = []; let markers: Marker[] = [];
let lines = output.split("\n"); let lines = output.split("\n");
let m = lines[0].match(/.*Process (.*)/); let m = lines[0].match(/.*Process (.*)/);
@@ -407,7 +468,9 @@ const processOutput = (
if (m) fn = m[1]; if (m) fn = m[1];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
const match = line.match(/(INFO|ERROR) at ([^:]+):\((\d+), (\d+)\):\s*(.*)/); const match = line.match(
/(INFO|ERROR) at ([^:]+):\((\d+), (\d+)\):\s*(.*)/
);
if (match) { if (match) {
let [_full, kind, file, line, col, message] = match; let [_full, kind, file, line, col, message] = match;
let lineNumber = +line + 1; let lineNumber = +line + 1;
@@ -428,7 +491,7 @@ const processOutput = (
} }
if (kind === "ERROR" || lineNumber) if (kind === "ERROR" || lineNumber)
markers.push({ markers.push({
severity: kind === 'ERROR' ? 'error' : 'info', severity: kind === "ERROR" ? "error" : "info",
message, message,
startLineNumber: lineNumber, startLineNumber: lineNumber,
endLineNumber: lineNumber, endLineNumber: lineNumber,
@@ -437,7 +500,7 @@ const processOutput = (
}); });
} }
} }
console.log('markers', markers) console.log("markers", markers);
// editor.setMarkers(markers) // editor.setMarkers(markers)
return markers; return markers;
}; };

View File

@@ -1,6 +1,8 @@
import { EditorView } from "codemirror"; import { EditorView } from "codemirror";
import { linter, Diagnostic } from "@codemirror/lint"; import { linter, Diagnostic } from "@codemirror/lint";
export interface CompileReq { export interface CompileReq {
id: string id: string
type: "compileRequest"; type: "compileRequest";

View File

@@ -1,25 +1,29 @@
import { shim } from "./emul"; import { shim } from "./emul";
import { API, Message, ResponseMSG } from "./ipc";
import { archive, preload } from "./preload"; import { archive, preload } from "./preload";
import { CompileReq, CompileRes } from "./types";
const LOG = console.log
console.log = (m) => { console.log = (m) => {
shim.stdout += "\n" + m; shim.stdout += "\n" + m;
}; };
const handleMessage = async function (ev: { data: CompileReq }) { const handleMessage = async function <K extends keyof API>(ev: { data: Message<K> }) {
console.log("message", ev.data); LOG("HANDLE", ev.data);
await preload; await preload;
shim.archive = archive; shim.archive = archive;
let { id, src, fileName } = ev.data; let key = ev.data.key
if (key === 'typeCheck' || key === 'compile') {
let {id, args: [fileName]} = ev.data
LOG(key, fileName)
const outfile = "out.js"; const outfile = "out.js";
if (ev.data.compile) const isCompile = key === 'compile';
if (isCompile)
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"]; shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
else else
shim.process.argv = ["browser", "newt", fileName, "--top"]; shim.process.argv = ["browser", "newt", fileName, "--top"];
shim.files[fileName] = new TextEncoder().encode(src);
shim.files[outfile] = new TextEncoder().encode("No JS output");
shim.stdout = ""; shim.stdout = "";
const start = +new Date(); shim.files[outfile] = new TextEncoder().encode("No JS output");
try { try {
Main_main(); Main_main();
} catch (e) { } catch (e) {
@@ -28,14 +32,19 @@ const handleMessage = async function (ev: { data: CompileReq }) {
// make it visable on page // make it visable on page
shim.stdout += "\n" + String(e); shim.stdout += "\n" + String(e);
} }
let duration = +new Date() - start; let result = isCompile ? new TextDecoder().decode(shim.files[outfile]) : shim.stdout
console.log(`process ${fileName} in ${duration} ms`); sendResponse({id, result})
let javascript = new TextDecoder().decode(shim.files[outfile]); } else if (key === 'save') {
let output = shim.stdout; let {id, args: [fileName, content]} = ev.data
sendResponse({ id, type: "compileResult", javascript, output, duration }); LOG(`SAVE ${content?.length} to ${fileName}`)
shim.files[fileName] = new TextEncoder().encode(content)
LOG('send', {id, result: ''})
sendResponse({id, result: ''})
}
}; };
// hooks for worker.html to override // hooks for worker.html to override
let sendResponse: (_: CompileRes) => void = postMessage; let sendResponse: <K extends keyof API>(_: ResponseMSG) => void = postMessage;
onmessage = handleMessage; onmessage = handleMessage;
importScripts("newt.js"); importScripts("newt.js");

View File

@@ -1,269 +1,11 @@
import { inflate } from "./inflate";
// I wrote this inflate years ago, seems to work for zip
class BitReader {
pos = 0;
bits = 0;
acc = 0;
len: number;
data: Uint8Array;
constructor(data: Uint8Array) {
this.data = data;
this.len = data.length;
}
read(bits: number) {
while (this.bits < bits) {
if (this.pos >= this.len) throw "EOF";
this.acc |= this.data[this.pos++] << this.bits;
this.bits += 8;
}
let rval = this.acc & ((1 << bits) - 1);
this.acc >>= bits;
this.bits -= bits;
return rval;
}
read8() {
if (this.pos > this.len) throw "EOF";
// flush
if (this.bits > 0) {
this.bits = 0;
this.acc = 0;
}
return this.data[this.pos++];
}
read16() {
let rval = this.read8() * 256 + this.read8();
return rval;
}
}
class HuffDic {
limit: number[];
codes: number[];
base: number[];
constructor(lengths: number[]) {
let counts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let min = 0;
let max = 0;
for (let i = 0; i < lengths.length; i++) {
let len = lengths[i];
if (len != 0) {
if (len < min || min == 0) min = len;
if (len > max) max = len;
counts[len]++;
}
}
this.base = [];
this.limit = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
];
let code = 0;
let seq = 0;
let next_code = [];
for (let i = min; i <= max; i++) {
let n = counts[i];
next_code[i] = code;
this.base[i] = code - seq;
code += n;
seq += n;
this.limit[i] = code - 1;
code <<= 1;
}
this.codes = [];
for (let i = 0; i < lengths.length; i++) {
let n = lengths[i];
if (n != 0) {
code = next_code[n];
next_code[n]++;
if (!this.base[n]) this.base[n] = 0;
seq = code - this.base[n];
this.codes[seq] = i;
}
}
}
readSymbol(r: BitReader) {
let v = 0;
let l = 0;
let offset = 0;
for (let i = 1; i < this.limit.length; i++) {
v <<= 1;
v |= r.read(1);
let limit = this.limit[i];
if (v <= limit) {
return this.codes[v - this.base[i]];
}
}
throw "eHUFF";
}
}
let staticHuff: HuffDic;
let distHuff: HuffDic;
{
let tmp = [];
for (let i = 0; i < 144; i++) tmp[i] = 8;
for (let i = 144; i < 256; i++) tmp[i] = 9;
for (let i = 256; i < 280; i++) tmp[i] = 7;
for (let i = 280; i < 288; i++) tmp[i] = 8;
staticHuff = new HuffDic(tmp);
tmp = [];
for (let i = 0; i < 30; i++) {
tmp[i] = 5;
}
distHuff = new HuffDic(tmp);
}
function inflate(input: Uint8Array) {
let r = new BitReader(input);
let out = new Uint8Array(65536);
let pos = 0;
const push = (b: number) => {
if (pos + 10 > out.length) {
const tmp = new Uint8Array(out.length * 1.5);
tmp.set(out);
out = tmp;
}
out[pos++] = b;
};
let fin = 0;
while (!fin) {
fin = r.read(1);
let btype = r.read(2);
let huff2;
let huff3;
if (btype == 0) {
let len = r.read16();
let nlen = r.read16();
for (let i = 0; i < len; i++) push(r.read8());
} else if (btype == 1) {
// fixed huffman
huff2 = staticHuff;
huff3 = distHuff;
} else if (btype == 2) {
// dynamic huffman
let hlit = r.read(5) + 257;
let hdist = r.read(5) + 1;
let hclen = r.read(4) + 4;
let lengths: number[] = [];
for (let i = 0; i < 19; i++) lengths[i] = 0;
let xx = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
for (let i = 0; i < hclen; i++) {
let t = r.read(3);
lengths[xx[i]] = t;
}
let huff = new HuffDic(lengths);
lengths = [];
while (true) {
let k = huff.readSymbol(r);
if (k < 16) {
lengths.push(k);
} else if (k == 16) {
let count = r.read(2) + 3;
if (lengths.length == 0) throw new Error("no lengths?");
for (; count > 0; count--) lengths.push(lengths[lengths.length - 1]);
} else if (k == 17) {
let count = r.read(3) + 3;
for (; count > 0; count--) lengths.push(0);
} else if (k == 18) {
let count = r.read(7) + 11;
for (; count > 0; count--) lengths.push(0);
}
if (lengths.length >= hlit + hdist) break;
}
huff2 = new HuffDic(lengths.slice(0, hlit));
huff3 = new HuffDic(lengths.slice(hlit));
} else {
throw new Error("btype " + btype);
}
if (huff2) {
while (true) {
let k = huff2.readSymbol(r);
let len = 0;
let n = 0; // extra bits
if (k < 256) {
push(k);
continue;
} else if (k == 256) {
// End of block
break;
} else if (k < 265) {
len = k - 257 + 3;
n = 0;
} else if (k < 269) {
len = (k - 265) * 2 + 11;
n = 1;
} else if (k < 273) {
len = (k - 269) * 4 + 19;
n = 2;
} else if (k < 277) {
len = (k - 273) * 8 + 35;
n = 3;
} else if (k < 281) {
len = (k - 277) * 16 + 67;
n = 4;
} else if (k < 285) {
len = (k - 281) * 32 + 131;
n = 5;
} else {
len = 258;
n = 0;
}
if (n > 0) len += r.read(n);
// distance
if (r.pos > r.len) throw new Error("EOF");
let dist;
if (huff3) dist = huff3.readSymbol(r);
else dist = r.read(5);
if (dist < 4) {
dist++;
} else if (dist < 30) {
let db = (dist - 2) >> 1;
let extra = (dist & 1) << db;
extra |= r.read(db);
dist = (1 << (db + 1)) + 1 + extra;
} else {
throw new Error(`dist ${dist}`);
}
if (dist > pos) throw new Error(`dist ${dist} > pos ${pos}`);
let s: number = pos - dist;
for (let i = 0; i < len; i++) push(out[s + i]);
}
}
}
return out.slice(0, pos);
}
interface Entry { interface Entry {
size: number; size: number;
start: number; start: number;
end: number; end: number;
method: number;
} }
export class ZipFile { export class ZipFile {
@@ -292,13 +34,12 @@ export class ZipFile {
if (size) { if (size) {
let start = pos + 30 + fnlen + eflen; let start = pos + 30 + fnlen + eflen;
let end = start + csize; let end = start + csize;
this.entries[fn] = { size, start, end }; this.entries[fn] = { size, start, end, method };
} }
pos = pos + 30 + fnlen + eflen + csize; pos = pos + 30 + fnlen + eflen + csize;
} }
} }
getData(name: string) { getData(name: string) {
if (!(name in this.entries)) return
let { start, end, size } = this.entries[name]; let { start, end, size } = this.entries[name];
return inflate(new Uint8Array(this.data.slice(start, end))); return inflate(new Uint8Array(this.data.slice(start, end)));
} }

View File

@@ -7,6 +7,31 @@ svg.icon path {
fill: none; fill: none;
} }
#help {
padding: 5px;
font-family: 'Comic Code', monospace;
}
#help>div {
margin: 5px 0;
}
.toast {
position: absolute;
left: 50%;
top: 50px;
width: auto;
z-index: 100;
background: white;
}
.toast div {
margin-left: -50%;
border: solid 2px darkgrey;
padding: 10px;
border-radius: 10px;
background: white;
text-align: center;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body { body {
color: white; color: white;
@@ -60,6 +85,7 @@ svg.icon path {
.tabPanel { .tabPanel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: 'Comic Code', monospace;
} }
.tabBar { .tabBar {
display:flex; display:flex;
@@ -93,3 +119,6 @@ svg.icon path {
.tooltip { .tooltip {
padding: 5px; padding: 5px;
} }
.cm-editor .cm-content {
font-family: 'Comic Code', monospace;
}