Playground enhancements
This commit is contained in:
14
playground/src/base64.test.ts
Normal file
14
playground/src/base64.test.ts
Normal 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
45
playground/src/base64.ts
Normal 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);
|
||||
}
|
||||
@@ -81,7 +81,6 @@ function tokenizer(stream: StringStream, state: State): string | null {
|
||||
let word = stream.current();
|
||||
if (keywords.includes(word)) return "keyword";
|
||||
if (word[0] >= "A" && word[0] <= "Z") return "typename";
|
||||
console.log('IDENT', )
|
||||
return "identifier";
|
||||
}
|
||||
// unhandled
|
||||
@@ -136,7 +135,7 @@ export class CMEditor implements AbstractEditor {
|
||||
// For indent on return
|
||||
indentService.of((ctx, pos) => {
|
||||
let line = ctx.lineAt(pos)
|
||||
if (!line) return null
|
||||
if (!line || !line.from) return null
|
||||
let prevLine = ctx.lineAt(line.from - 1);
|
||||
if (prevLine.text.trimEnd().match(/\b(of|where|do)\s*$/)) {
|
||||
let pindent = prevLine.text.match(/^\s*/)?.[0].length ?? 0
|
||||
|
||||
131
playground/src/deflate.ts
Normal file
131
playground/src/deflate.ts
Normal 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)
|
||||
}
|
||||
@@ -9,7 +9,14 @@ export interface Handle {
|
||||
|
||||
interface Process {
|
||||
argv: string[];
|
||||
platform: string;
|
||||
exit(_: number): void;
|
||||
stdout: {
|
||||
write(s: string): unknown
|
||||
};
|
||||
cwd(): string;
|
||||
env: Record<string,string>
|
||||
__lasterr: {errno: number}
|
||||
}
|
||||
export interface NodeShim {
|
||||
stdout: string;
|
||||
|
||||
@@ -62,7 +62,7 @@ window.addEventListener("message", (ev: MessageEvent<Message>) => {
|
||||
let { src } = ev.data;
|
||||
try {
|
||||
sendMessage({ type: "setConsole", messages: [] });
|
||||
eval(src);
|
||||
(new Function(src))();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
16
playground/src/inflate.test.ts
Normal file
16
playground/src/inflate.test.ts
Normal 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
304
playground/src/inflate.ts
Normal 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
74
playground/src/ipc.ts
Normal 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 {}
|
||||
@@ -1,32 +1,59 @@
|
||||
import { effect, signal } from "@preact/signals";
|
||||
import { Diagnostic } from "@codemirror/lint";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { h, render } from "preact";
|
||||
import { h, render, VNode } from "preact";
|
||||
import { ChangeEvent } from "preact/compat";
|
||||
import { archive, preload } from "./preload.ts";
|
||||
import { b64decode, b64encode } from "./base64";
|
||||
import {
|
||||
AbstractEditor,
|
||||
EditorDelegate,
|
||||
CompileReq,
|
||||
CompileRes,
|
||||
Message,
|
||||
TopData,
|
||||
Marker,
|
||||
} from "./types.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;
|
||||
|
||||
const newtWorker = new Worker("worker.js");
|
||||
// :FIXME use because Safari
|
||||
let postMessage = (msg: CompileReq) => 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);
|
||||
postMessage = (msg: any) => workerFrame.contentWindow?.postMessage(msg, "*");
|
||||
const ipc = new IPC();
|
||||
|
||||
function mdline2nodes(s: string) {
|
||||
let cs: (VNode|string)[] = []
|
||||
let toks = s.matchAll(/(\*\*.*?\*\*)|(\*.*?\*)|(_.*?_)|[^*]+|\*/g)
|
||||
for (let tok of toks) {
|
||||
if (tok[1]) cs.push(h('b',{},tok[0].slice(2,-2)))
|
||||
else if (tok[2]) cs.push(h('em',{},tok[0].slice(1,-1)))
|
||||
else if (tok[3]) cs.push(h('em',{},tok[0].slice(1,-1)))
|
||||
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
|
||||
@@ -35,13 +62,20 @@ iframe.src = "frame.html";
|
||||
iframe.style.display = "none";
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
function run(src: string) {
|
||||
console.log("SEND TO", iframe.contentWindow);
|
||||
const fileName = state.currentFile.value;
|
||||
// postMessage({ type: "compileRequest", fileName, src });
|
||||
async function refreshJS() {
|
||||
if (!state.javascript.value) {
|
||||
let src = state.editor.value!.getValue();
|
||||
console.log("SEND TO", iframe.contentWindow);
|
||||
const fileName = state.currentFile.value;
|
||||
// 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;
|
||||
console.log("RUN", iframe.contentWindow);
|
||||
try {
|
||||
@@ -68,55 +102,47 @@ function setOutput(output: string) {
|
||||
state.output.value = output;
|
||||
}
|
||||
|
||||
let lastID = 0;
|
||||
const nextID = () => "" + lastID++;
|
||||
|
||||
window.onmessage = (ev: MessageEvent<Message>) => {
|
||||
window.addEventListener("message", (ev) => {
|
||||
if (ev.data.id) return;
|
||||
console.log("window got", ev.data);
|
||||
if ("messages" in ev.data) state.messages.value = ev.data.messages;
|
||||
if ("message" in ev.data) {
|
||||
state.messages.value = [...state.messages.value, ev.data.message];
|
||||
}
|
||||
// safari callback
|
||||
if ("output" in ev.data) {
|
||||
newtWorker.onmessage?.(ev)
|
||||
setOutput(ev.data.output);
|
||||
state.javascript.value = ev.data.javascript;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// TODO wrap up IPC
|
||||
async function copyToClipboard(ev: Event) {
|
||||
ev.preventDefault();
|
||||
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);
|
||||
}
|
||||
|
||||
type Suspense<T> = {
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
// We could push this into the editor
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
if ((ev.metaKey || ev.ctrlKey) && ev.code == "KeyS")
|
||||
copyToClipboard(ev);
|
||||
});
|
||||
|
||||
const callbacks: Record<string, Suspense<string>> = {};
|
||||
|
||||
newtWorker.onmessage = (ev: MessageEvent<CompileRes>) => {
|
||||
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);
|
||||
function getSavedCode() {
|
||||
let value: string = localStorage.idrisCode || LOADING;
|
||||
let hash = window.location.hash;
|
||||
if (hash.startsWith("#code/")) {
|
||||
try {
|
||||
value = new TextDecoder().decode(inflate(b64decode(hash.slice(6))));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const state = {
|
||||
output: signal(""),
|
||||
toast: signal(""),
|
||||
javascript: signal(""),
|
||||
messages: signal<string[]>([]),
|
||||
editor: signal<AbstractEditor | null>(null),
|
||||
@@ -166,7 +192,7 @@ document.addEventListener("keydown", (ev) => {
|
||||
|
||||
const LOADING = "module Loading\n";
|
||||
|
||||
let value = localStorage.code || LOADING;
|
||||
let value = getSavedCode() || LOADING;
|
||||
let initialVertical = localStorage.vertical == "true";
|
||||
|
||||
interface EditorProps {
|
||||
@@ -176,13 +202,8 @@ const language: EditorDelegate = {
|
||||
getEntry(word, _row, _col) {
|
||||
return topData?.context.find((entry) => entry.name === word);
|
||||
},
|
||||
onChange(value) {
|
||||
// run via the linter now
|
||||
// clearTimeout(timeout);
|
||||
// timeout = setTimeout(() => {
|
||||
// run(value);
|
||||
// localStorage.code = value;
|
||||
// }, 1000);
|
||||
onChange(_value) {
|
||||
// we're using lint() now
|
||||
},
|
||||
getFileName() {
|
||||
if (!topData) return "";
|
||||
@@ -192,39 +213,35 @@ const language: EditorDelegate = {
|
||||
async lint(view) {
|
||||
console.log("LINT");
|
||||
let src = view.state.doc.toString();
|
||||
localStorage.code = src
|
||||
let module = src.match(/module\s+([^\s]+)/)?.[1]
|
||||
localStorage.code = src;
|
||||
let module = src.match(/module\s+([^\s]+)/)?.[1];
|
||||
if (module) {
|
||||
// This causes problems with stuff like aoc/... that reference files in the same directory
|
||||
// state.currentFile.value = module.replace('.','/')+'.newt'
|
||||
// This causes problems with stuff like aoc/...
|
||||
state.currentFile.value = module.replace(".", "/") + ".newt";
|
||||
}
|
||||
state.javascript.value = ''
|
||||
let fileName = state.currentFile.value;
|
||||
console.log("FN", fileName);
|
||||
try {
|
||||
let out = await runCommand({
|
||||
id: nextID(),
|
||||
type: "compileRequest",
|
||||
fileName,
|
||||
src,
|
||||
compile: false,
|
||||
});
|
||||
console.log("OUT", out);
|
||||
await ipc.sendMessage("save", [fileName, src]);
|
||||
let out = await ipc.sendMessage("typeCheck", [fileName]);
|
||||
setOutput(out);
|
||||
let markers = processOutput(out);
|
||||
let diags: Diagnostic[] = []
|
||||
let diags: Diagnostic[] = [];
|
||||
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;
|
||||
let word = view.state.wordAt(pos)
|
||||
let word = view.state.wordAt(pos);
|
||||
diags.push({
|
||||
from: word?.from ?? pos,
|
||||
to: word?.to ?? pos+1,
|
||||
to: word?.to ?? pos + 1,
|
||||
severity: marker.severity,
|
||||
message: marker.message,
|
||||
})
|
||||
});
|
||||
}
|
||||
return diags
|
||||
return diags;
|
||||
} catch (e) {
|
||||
console.log("ERR", e);
|
||||
}
|
||||
@@ -243,7 +260,6 @@ function Editor({ initialValue }: EditorProps) {
|
||||
state.editor.value = editor;
|
||||
editor.setDark(state.dark.value);
|
||||
if (initialValue === LOADING) loadFile("Tour.newt");
|
||||
else run(initialValue);
|
||||
}, []);
|
||||
|
||||
return h("div", { id: "editor", ref });
|
||||
@@ -260,6 +276,38 @@ function Result() {
|
||||
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() {
|
||||
const messages = state.messages.value ?? [];
|
||||
return h(
|
||||
@@ -272,6 +320,7 @@ function Console() {
|
||||
const RESULTS = "Output";
|
||||
const JAVASCRIPT = "JS";
|
||||
const CONSOLE = "Console";
|
||||
const HELP = "Help";
|
||||
|
||||
function Tabs() {
|
||||
const [selected, setSelected] = useState(localStorage.tab ?? RESULTS);
|
||||
@@ -289,6 +338,10 @@ function Tabs() {
|
||||
if (state.messages.value.length) setSelected(CONSOLE);
|
||||
}, [state.messages.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected === JAVASCRIPT && !state.javascript.value) refreshJS();
|
||||
}, [selected, state.javascript.value]);
|
||||
|
||||
let body;
|
||||
switch (selected) {
|
||||
case RESULTS:
|
||||
@@ -300,6 +353,9 @@ function Tabs() {
|
||||
case CONSOLE:
|
||||
body = h(Console, {});
|
||||
break;
|
||||
case HELP:
|
||||
body = h(Help, {});
|
||||
break;
|
||||
default:
|
||||
body = h("div", {});
|
||||
}
|
||||
@@ -312,7 +368,8 @@ function Tabs() {
|
||||
{ className: "tabBar" },
|
||||
Tab(RESULTS),
|
||||
Tab(JAVASCRIPT),
|
||||
Tab(CONSOLE)
|
||||
Tab(CONSOLE),
|
||||
Tab(HELP),
|
||||
),
|
||||
h("div", { className: "tabBody" }, body)
|
||||
);
|
||||
@@ -348,12 +405,15 @@ function EditWrap({
|
||||
loadFile(fn);
|
||||
}
|
||||
};
|
||||
let d = vertical
|
||||
? "M0 0 h20 v20 h-20 z M0 10 h20"
|
||||
: "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 }));
|
||||
|
||||
const char = vertical ? "↕" : "↔"
|
||||
|
||||
// let d = vertical
|
||||
// ? "M0 0 h20 v20 h-20 z M0 10 h20"
|
||||
// : "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(
|
||||
"div",
|
||||
{ className: "tabPanel left" },
|
||||
@@ -366,8 +426,10 @@ function EditWrap({
|
||||
options
|
||||
),
|
||||
h("div", { style: { flex: "1 1" } }),
|
||||
h("button", { onClick: runOutput }, svg(play)),
|
||||
h("button", { onClick: toggle }, svg(d))
|
||||
h("button", { onClick: copyToClipboard, title: "copy url" }, "📋"),
|
||||
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 }))
|
||||
);
|
||||
@@ -379,10 +441,15 @@ function App() {
|
||||
setVertical(!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"}`;
|
||||
return h(
|
||||
"div",
|
||||
{ className },
|
||||
toast,
|
||||
h(EditWrap, { vertical, toggle }),
|
||||
h(Tabs, {})
|
||||
);
|
||||
@@ -390,16 +457,10 @@ function 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 = (
|
||||
// editor: AbstractEditor,
|
||||
output: string
|
||||
) => {
|
||||
// let model = editor.getModel()!;
|
||||
console.log('process output', output)
|
||||
console.log("process output", output);
|
||||
let markers: Marker[] = [];
|
||||
let lines = output.split("\n");
|
||||
let m = lines[0].match(/.*Process (.*)/);
|
||||
@@ -407,7 +468,9 @@ const processOutput = (
|
||||
if (m) fn = m[1];
|
||||
for (let i = 0; i < lines.length; 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) {
|
||||
let [_full, kind, file, line, col, message] = match;
|
||||
let lineNumber = +line + 1;
|
||||
@@ -428,7 +491,7 @@ const processOutput = (
|
||||
}
|
||||
if (kind === "ERROR" || lineNumber)
|
||||
markers.push({
|
||||
severity: kind === 'ERROR' ? 'error' : 'info',
|
||||
severity: kind === "ERROR" ? "error" : "info",
|
||||
message,
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
@@ -437,7 +500,7 @@ const processOutput = (
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('markers', markers)
|
||||
console.log("markers", markers);
|
||||
// editor.setMarkers(markers)
|
||||
return markers;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { EditorView } from "codemirror";
|
||||
import { linter, Diagnostic } from "@codemirror/lint";
|
||||
|
||||
|
||||
|
||||
export interface CompileReq {
|
||||
id: string
|
||||
type: "compileRequest";
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
import { shim } from "./emul";
|
||||
import { API, Message, ResponseMSG } from "./ipc";
|
||||
import { archive, preload } from "./preload";
|
||||
import { CompileReq, CompileRes } from "./types";
|
||||
|
||||
const LOG = console.log
|
||||
console.log = (m) => {
|
||||
shim.stdout += "\n" + m;
|
||||
};
|
||||
|
||||
const handleMessage = async function (ev: { data: CompileReq }) {
|
||||
console.log("message", ev.data);
|
||||
const handleMessage = async function <K extends keyof API>(ev: { data: Message<K> }) {
|
||||
LOG("HANDLE", ev.data);
|
||||
await preload;
|
||||
shim.archive = archive;
|
||||
let { id, src, fileName } = ev.data;
|
||||
const outfile = "out.js";
|
||||
if (ev.data.compile)
|
||||
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
|
||||
else
|
||||
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 = "";
|
||||
const start = +new Date();
|
||||
try {
|
||||
Main_main();
|
||||
} catch (e) {
|
||||
// make it clickable in console
|
||||
console.error(e);
|
||||
// make it visable on page
|
||||
shim.stdout += "\n" + String(e);
|
||||
let key = ev.data.key
|
||||
if (key === 'typeCheck' || key === 'compile') {
|
||||
let {id, args: [fileName]} = ev.data
|
||||
LOG(key, fileName)
|
||||
const outfile = "out.js";
|
||||
const isCompile = key === 'compile';
|
||||
if (isCompile)
|
||||
shim.process.argv = ["browser", "newt", fileName, "-o", outfile, "--top"];
|
||||
else
|
||||
shim.process.argv = ["browser", "newt", fileName, "--top"];
|
||||
shim.stdout = "";
|
||||
shim.files[outfile] = new TextEncoder().encode("No JS output");
|
||||
|
||||
try {
|
||||
Main_main();
|
||||
} catch (e) {
|
||||
// make it clickable in console
|
||||
console.error(e);
|
||||
// make it visable on page
|
||||
shim.stdout += "\n" + String(e);
|
||||
}
|
||||
let result = isCompile ? new TextDecoder().decode(shim.files[outfile]) : shim.stdout
|
||||
sendResponse({id, result})
|
||||
} else if (key === 'save') {
|
||||
let {id, args: [fileName, content]} = ev.data
|
||||
LOG(`SAVE ${content?.length} to ${fileName}`)
|
||||
shim.files[fileName] = new TextEncoder().encode(content)
|
||||
LOG('send', {id, result: ''})
|
||||
sendResponse({id, result: ''})
|
||||
}
|
||||
let duration = +new Date() - start;
|
||||
console.log(`process ${fileName} in ${duration} ms`);
|
||||
let javascript = new TextDecoder().decode(shim.files[outfile]);
|
||||
let output = shim.stdout;
|
||||
sendResponse({ id, type: "compileResult", javascript, output, duration });
|
||||
|
||||
};
|
||||
|
||||
// hooks for worker.html to override
|
||||
let sendResponse: (_: CompileRes) => void = postMessage;
|
||||
let sendResponse: <K extends keyof API>(_: ResponseMSG) => void = postMessage;
|
||||
onmessage = handleMessage;
|
||||
importScripts("newt.js");
|
||||
|
||||
@@ -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 {
|
||||
size: number;
|
||||
start: number;
|
||||
end: number;
|
||||
method: number;
|
||||
}
|
||||
|
||||
export class ZipFile {
|
||||
@@ -292,13 +34,12 @@ export class ZipFile {
|
||||
if (size) {
|
||||
let start = pos + 30 + fnlen + eflen;
|
||||
let end = start + csize;
|
||||
this.entries[fn] = { size, start, end };
|
||||
this.entries[fn] = { size, start, end, method };
|
||||
}
|
||||
pos = pos + 30 + fnlen + eflen + csize;
|
||||
}
|
||||
}
|
||||
getData(name: string) {
|
||||
if (!(name in this.entries)) return
|
||||
let { start, end, size } = this.entries[name];
|
||||
return inflate(new Uint8Array(this.data.slice(start, end)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user