From bc6402ee288fc532eb8fc7cfddce17feba60ba45 Mon Sep 17 00:00:00 2001 From: weareoutman Date: Thu, 21 Sep 2023 15:12:47 +0800 Subject: [PATCH] Compress shared example with gzip --- src/components/NextExample/index.tsx | 36 ++++++++++++++++++++-------- src/pages/playground/index.tsx | 23 ++++++++++++++---- src/utils/b64Unicode.ts | 33 +++++++++++++------------ src/utils/gzip.ts | 18 ++++++++++++++ 4 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 src/utils/gzip.ts diff --git a/src/components/NextExample/index.tsx b/src/components/NextExample/index.tsx index 35b069a4..d53607d1 100644 --- a/src/components/NextExample/index.tsx +++ b/src/components/NextExample/index.tsx @@ -17,8 +17,8 @@ import clsx from "clsx"; import useDeferredValue from "@site/src/hooks/useDeferredValue"; import { EXAMPLE_IFRAME_MIN_HEIGHT } from "@site/src/constants"; import getContentHeightByCode from "@site/src/utils/getContentHeightByCode"; -import { b64EncodeUnicode } from "@site/src/utils/b64Unicode"; import useExampleLanguage from "@site/src/hooks/useExampleLanguage"; +import { GZIP_HASH_PREFIX, compress } from "@site/src/utils/gzip"; import ChevronUp from "./chevron-up.svg"; import ChevronDown from "./chevron-down.svg"; import styles from "./styles.module.css"; @@ -172,6 +172,30 @@ export default function NextExample({ setSourceShown((prev) => !prev); }, []); + const [playgroundUrl, setPlaygroundUrl] = useState("/playground"); + + useEffect(() => { + let ignore = false; + async function updatePlaygroundUrl() { + try { + const url = `/playground${GZIP_HASH_PREFIX}${await compress( + JSON.stringify({ + [language]: currentCode, + }) + )}`; + if (!ignore) { + setPlaygroundUrl(url); + } + } catch (e) { + console.error("Compress shared example failed:", e); + } + } + updatePlaygroundUrl(); + return () => { + ignore = true; + }; + }, [currentCode, language]); + return (
@@ -256,15 +280,7 @@ export default function NextExample({ > YAML - + Playground diff --git a/src/pages/playground/index.tsx b/src/pages/playground/index.tsx index ec0b8ee5..37bc345b 100644 --- a/src/pages/playground/index.tsx +++ b/src/pages/playground/index.tsx @@ -16,8 +16,9 @@ import LoadingRing from "@site/src/components/LoadingRing"; import useDeferredValue from "@site/src/hooks/useDeferredValue"; import examplesJson from "@site/src/examples.json"; import usePlaygroundQuery from "@site/src/hooks/usePlaygroundQuery"; -import { b64DecodeUnicode, b64EncodeUnicode } from "@site/src/utils/b64Unicode"; +import { b64DecodeUnicode } from "@site/src/utils/b64Unicode"; import { decorateAltCode } from "@site/src/utils/decorateAltCode"; +import { GZIP_HASH_PREFIX, compress, decompress } from "@site/src/utils/gzip"; import styles from "./style.module.css"; const { examples } = examplesJson; @@ -34,6 +35,17 @@ const STORAGE_KEY_CODES = { }; const SHARE_TEXT = "Share"; +let decompressedExampleString: string; +if (location.hash.startsWith(GZIP_HASH_PREFIX)) { + try { + decompressedExampleString = await decompress( + location.hash.substring(GZIP_HASH_PREFIX.length) + ); + } catch (e) { + console.error("Decompress shared example failed:", e); + } +} + export default function PlaygroundPage(): JSX.Element { return ( @@ -176,11 +188,11 @@ function Playground(): JSX.Element { [isLocal, mode] ); - const handleShare = useCallback(() => { + const handleShare = useCallback(async () => { history.replaceState( null, "", - `#${b64EncodeUnicode( + `${GZIP_HASH_PREFIX}${await compress( JSON.stringify({ [mode]: currentCode, gap: hasGap, @@ -357,7 +369,10 @@ function useInitialExample(): InitialExample { if (hash) { let sharedExample: InitialExample; try { - sharedExample = JSON.parse(b64DecodeUnicode(location.hash.slice(1))); + sharedExample = JSON.parse( + decompressedExampleString ?? + b64DecodeUnicode(location.hash.slice(1)) + ); } catch (error) { // eslint-disable-next-line no-console console.error("Parse pasted sources failed:", error); diff --git a/src/utils/b64Unicode.ts b/src/utils/b64Unicode.ts index 5a6678a5..c5e8cc97 100644 --- a/src/utils/b64Unicode.ts +++ b/src/utils/b64Unicode.ts @@ -1,20 +1,19 @@ -export function b64EncodeUnicode(str: string) { - // first we use encodeURIComponent to get percent-encoded UTF-8, - // then we convert the percent encodings into raw bytes which - // can be fed into btoa. - return btoa( - encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { - return String.fromCharCode(parseInt(p1, 16)); - }) - ); +// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + +export function b64EncodeUnicode(str: string): string { + return bytesToBase64(new TextEncoder().encode(str)); +} + +export function b64DecodeUnicode(str: string): string { + return new TextDecoder().decode(base64ToBytes(str)); +} + +export function base64ToBytes(base64: string): Uint8Array { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); } -export function b64DecodeUnicode(str: string) { - return decodeURIComponent( - [...atob(str)] - .map(function (c) { - return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); - }) - .join("") - ); +export function bytesToBase64(bytes: Uint8Array): string { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(""); + return btoa(binString); } diff --git a/src/utils/gzip.ts b/src/utils/gzip.ts new file mode 100644 index 00000000..30313528 --- /dev/null +++ b/src/utils/gzip.ts @@ -0,0 +1,18 @@ +import { base64ToBytes, bytesToBase64 } from "./b64Unicode"; + +export const GZIP_HASH_PREFIX = "#gzip,"; + +export async function compress(str: string) { + const blob = new Blob([str], { type: "text/plain" }); + const stream = blob.stream().pipeThrough(new CompressionStream("gzip")); + const compressedBlob = await new Response(stream).blob(); + return bytesToBase64(new Uint8Array(await compressedBlob.arrayBuffer())); +} + +export async function decompress(str: string) { + const bytes = base64ToBytes(str); + const blob = new Blob([bytes], { type: "text/plain" }); + const stream = blob.stream().pipeThrough(new DecompressionStream("gzip")); + const decompressedBlob = await new Response(stream).blob(); + return decompressedBlob.text(); +}