File size: 3,160 Bytes
794b22f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c4637f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* 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 };
}