dssl / src /lib /share.ts
EHS-Studio's picture
Claude Sonnet 5.5
v0.10.0: glass, tilt, named-colour alpha fix; playground with HTML tab, examples, warnings and CSS view; live effects on the home page
0c4637f
Raw History Blame Contribute Delete
3.16 kB
/* Share links: the playground source lives in the URL hash, never on a server.
*
* https://dssl-sable.vercel.app/playground#s=zH4sIAAAA...
*
* The first character says how the rest is packed: "z" is gzip (when the
* browser has CompressionStream, which is nearly all of them), "p" is plain
* UTF-8. Both are base64url, so the link survives chat apps and email. */
const toB64 = (bytes: Uint8Array): string => {
let bin = "";
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
const fromB64 = (s: string): Uint8Array => {
const pad = s.length % 4 ? "=".repeat(4 - (s.length % 4)) : "";
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
};
async function pipe(bytes: Uint8Array, stream: CompressionStream | DecompressionStream): Promise<Uint8Array> {
const w = stream.writable.getWriter();
void w.write(bytes as unknown as BufferSource);
void w.close();
return new Uint8Array(await new Response(stream.readable).arrayBuffer());
}
/** pack text for the URL hash */
export async function encodeShare(text: string): Promise<string> {
const bytes = new TextEncoder().encode(text);
if (typeof CompressionStream !== "undefined") {
try { return "z" + toB64(await pipe(bytes, new CompressionStream("gzip"))); } catch { /* fall through to plain */ }
}
return "p" + toB64(bytes);
}
/** unpack what encodeShare made; null when it is not ours or is damaged */
export async function decodeShare(packed: string): Promise<string | null> {
try {
const kind = packed[0];
const bytes = fromB64(packed.slice(1));
if (kind === "p") return new TextDecoder().decode(bytes);
if (kind === "z" && typeof DecompressionStream !== "undefined") return new TextDecoder().decode(await pipe(bytes, new DecompressionStream("gzip")));
} catch { /* damaged link */ }
return null;
}
/** the share payload in a location hash like "#s=zABC", or null */
export function shareFromHash(hash: string): string | null {
const m = /^#s=([A-Za-z0-9_-]+)$/.exec(hash);
return m ? m[1] : null;
}
/** a browser URL length that every chat app and server will accept */
export const MAX_SHARE_URL = 8000;
/** what a playground link holds: the styles, and the canvas HTML when it is not the default */
export type SharePayload = { css: string; html?: string };
/** plain text stays plain text (so old links keep working); anything with HTML is a small JSON object */
export const packPayload = (p: SharePayload): string => (p.html === undefined ? p.css : JSON.stringify({ v: 1, css: p.css, html: p.html }));
export function unpackPayload(text: string): SharePayload {
if (text.startsWith('{"v":1,')) {
try {
const o = JSON.parse(text);
if (o && typeof o.css === "string") return { css: o.css, html: typeof o.html === "string" ? o.html : undefined };
} catch { /* it was just a stylesheet that starts oddly */ }
}
return { css: text };
}