thurtle: Add URL compression

This commit is contained in:
Remko Tronçon 2024-01-21 17:45:46 +01:00
parent 41f3cf653c
commit 7135ea2865
2 changed files with 130 additions and 9 deletions

View file

@ -1,6 +1,7 @@
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
// Query parameters: // Query parameters:
// `p`: Base64-encoded program // `p`: Base64-encoded program
// `u`: Base64-encoded ULZ-compressed program
// `pn`: Program name. If `p` is not provided, looks up builtin example // `pn`: Program name. If `p` is not provided, looks up builtin example
// `ar`: Auto-run program // `ar`: Auto-run program
// `sn`: Show navbar (default: 1) // `sn`: Show navbar (default: 1)
@ -18,9 +19,12 @@ import {
import Editor from "./Editor"; import Editor from "./Editor";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import draw from "./draw"; import draw from "./draw";
import { ulzEncode, ulzDecode } from "./ulz";
declare let bootstrap: any; declare let bootstrap: any;
////////////////////////////////////////////////////////////////////////////////
async function b64encode(bs: Uint8Array, forURL?: boolean) { async function b64encode(bs: Uint8Array, forURL?: boolean) {
const url = await new Promise<string>((resolve) => { const url = await new Promise<string>((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
@ -46,6 +50,8 @@ function b64decode(s: string) {
); );
} }
////////////////////////////////////////////////////////////////////////////////
function parseQS(sqs = window.location.search) { function parseQS(sqs = window.location.search) {
const qs: Record<string, string> = {}; const qs: Record<string, string> = {};
const sqss = sqs const sqss = sqs
@ -65,6 +71,8 @@ function parseQS(sqs = window.location.search) {
const qs = parseQS(); const qs = parseQS();
////////////////////////////////////////////////////////////////////////////////
function About() { function About() {
return ( return (
<> <>
@ -604,13 +612,17 @@ async function downloadPNG(ev: MouseEvent) {
async function share(ev: MouseEvent) { async function share(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
shareModalTitleEl.innerText = programsEl.value; shareModalTitleEl.innerText = programsEl.value;
shareModalURLEl.value = `${window.location.protocol}//${ let url = `${window.location.protocol}//${window.location.host}${
window.location.host window.location.pathname
}${window.location.pathname}?pn=${encodeURIComponent( }?pn=${encodeURIComponent(programsEl.value)}&ar=1&`;
programsEl.value const program = new TextEncoder().encode(editor.getValue());
)}&p=${encodeURIComponent( const compressed = ulzEncode(program);
await b64encode(new TextEncoder().encode(editor.getValue()), true) if (compressed.length < program.length) {
)}&ar=1`; url += `u=${encodeURIComponent(await b64encode(compressed, true))}`;
} else {
url += `p=${encodeURIComponent(await b64encode(program, true))}`;
}
shareModalURLEl.value = url;
shareModal.show(); shareModal.show();
} }
@ -676,8 +688,9 @@ async function reset() {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
if (qs.p) { if (qs.p || qs.u) {
saveProgram(qs.pn ?? "", new TextDecoder().decode(b64decode(qs.p)), true); const program = qs.p ? b64decode(qs.p) : ulzDecode(b64decode(qs.u));
saveProgram(qs.pn ?? "", new TextDecoder().decode(program), true);
loadPrograms(); loadPrograms();
loadProgram(qs.pn); loadProgram(qs.pn);
} else { } else {

108
src/web/thurtle/ulz.ts Normal file
View file

@ -0,0 +1,108 @@
/*
* ULZ ( http://wiki.xxiivv.com/site/ulz_format ) encoder & decoder
*/
export function ulzDecode(src: Uint8Array) {
const dst: Array<number> = [];
let sp = 0;
while (sp < src.length) {
const c = src[sp++];
if (c & 0x80) {
// CPY
let length;
if (c & 0x40) {
if (sp >= src.length) {
throw new Error(`incomplete CPY2`);
}
length = ((c & 0x3f) << 8) | src[sp++];
} else {
length = c & 0x3f;
}
if (sp >= src.length) {
throw new Error(`incomplete CPY`);
}
let cp = dst.length - (src[sp++] + 1);
if (cp < 0) {
throw new Error(`CPY underflow`);
}
for (let i = 0; i < length + 4; i++) {
dst.push(dst[cp++]);
}
} else {
// LIT
if (sp + c >= src.length) {
throw new Error(`LIT out of bounds: ${sp} + ${c} >= ${src.length}`);
}
for (let i = 0; i < c + 1; i++) {
dst.push(src[sp++]);
}
}
}
return new Uint8Array(dst);
}
const MIN_MAX_LENGTH = 4;
function findBestMatch(
src: Uint8Array,
sp: number,
dlen: number,
slen: number
) {
let bmlen = 0;
let bmp = 0;
let dp = sp - dlen;
for (; dlen; dp++, dlen--) {
let i = 0;
for (; ; i++) {
if (i == slen) {
return [dp, i];
}
if (src[sp + i] != src[dp + (i % dlen)]) {
break;
}
}
if (i > bmlen) {
bmlen = i;
bmp = dp;
}
}
return [bmp, bmlen];
}
export function ulzEncode(src: Uint8Array) {
const dst: Array<number> = [];
let sp = 0;
let litp = -1;
while (sp < src.length) {
const dlen = Math.min(sp, 256);
const slen = Math.min(src.length - sp, 0x3fff + MIN_MAX_LENGTH);
const [bmp, bmlen] = findBestMatch(src, sp, dlen, slen);
if (bmlen >= MIN_MAX_LENGTH) {
// CPY
const bmctl = bmlen - MIN_MAX_LENGTH;
if (bmctl > 0x3f) {
// CPY2
dst.push((bmctl >> 8) | 0xc0);
dst.push(bmctl & 0xff);
} else {
dst.push(bmctl | 0x80);
}
dst.push(sp - bmp - 1);
sp += bmlen;
litp = -1;
} else {
// LIT
if (litp >= 0) {
if ((dst[litp] += 1) == 127) {
litp = -1;
}
} else {
dst.push(0);
litp = dst.length - 1;
}
dst.push(src[sp++]);
}
}
return new Uint8Array(dst);
}