Skip to content

Commit

Permalink
Compress Sass playground URL (#811)
Browse files Browse the repository at this point in the history
* Compress Sass playground URL

Uses 'pako' to compress the playground state into a shorter base64
representation. Also decompresses the URL back into the playground
when loading the first time.

If the output can't be decompressed, it'll be assumed to be already
decompressed for backwards compatibility and shown as-is.

* Add debounce when generating URL.

Also measured impact using '_playground.scss' with 7k characters resulted in a 14k-long URL. Now it only generates 2.6k.

* Add note about falling back to the original URL decoding strategy
  • Loading branch information
Goodwine authored Aug 30, 2023
1 parent 5684128 commit cca726a
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 6 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@types/markdown-it-attrs": "^4.1.0",
"@types/markdown-it-footnote": "^3.0.0",
"@types/node": "^16",
"@types/pako": "^2.0.0",
"@types/prismjs": "^1.26.0",
"@types/seedrandom": "^3.0.5",
"@types/semver": "^7.5.0",
Expand Down Expand Up @@ -99,6 +100,7 @@
"typogr": "^0.6.8"
},
"dependencies": {
"pako": "^2.1.0",
"seedrandom": "^3.0.5"
}
}
4 changes: 3 additions & 1 deletion source/assets/js/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function setupPlayground() {
debouncedUpdateCSS();
}
if (['inputFormat', 'outputFormat', 'inputValue'].includes(prop)) {
updateURL();
debounceUpdateURL();
}
return set;
},
Expand Down Expand Up @@ -234,6 +234,8 @@ function setupPlayground() {
}
}

const debounceUpdateURL = debounce(updateURL, 200);

function updateURL() {
const hash = stateToBase64(playgroundState);
history.replaceState('playground', '', `#${hash}`);
Expand Down
39 changes: 34 additions & 5 deletions source/assets/js/playground/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable node/no-extraneous-import */
import {Diagnostic} from '@codemirror/lint';
import {Exception, Importer, OutputStyle, Syntax} from 'sass';
import {deflate, inflate} from 'pako';

import {ConsoleLog, ConsoleLogDebug, ConsoleLogWarning} from './console-utils';

Expand All @@ -23,16 +24,44 @@ export function stateToBase64(state: PlaygroundState): string {
const inputFormatChar = state.inputFormat === 'scss' ? 1 : 0;
const outputFormatChar = state.outputFormat === 'expanded' ? 1 : 0;
const persistedState = `${inputFormatChar}${outputFormatChar}${state.inputValue}`;
return btoa(encodeURIComponent(persistedState));
return deflateToBase64(persistedState);
}

export function base64ToState(string: string): Partial<PlaygroundState> {
/** Compresses `input` and returns a base64 string of the compressed bytes. */
function deflateToBase64(input: string): string {
const deflated = deflate(input);
// btoa() input can't contain multi-byte characters, so it must be manually
// decoded into an ASCII string. TextDecoder can't take multi-byte characters,
// and encodeURIComponent doesn't escape all characters.
return btoa(String.fromCharCode(...deflated));
}

/** Decompresses a base64 `input` into the original string. */
function inflateFromBase64(input: string): string {
const base64 = atob(input);
const deflated = new Uint8Array(base64.length);
// Manually encode the inflated string because it was manually decoded without
// TextDecode. TextEncoder would generate a different representation for the
// given input.
for (let i = 0; i < base64.length; i++) {
deflated[i] = base64.charCodeAt(i);
}
return inflate(deflated, {to: 'string'});
}

export function base64ToState(input: string): Partial<PlaygroundState> {
const state: Partial<PlaygroundState> = {};
let decoded;
let decoded: string;
try {
decoded = decodeURIComponent(atob(string));
decoded = inflateFromBase64(input);
} catch (error) {
return {};
// For backwards compatibility, decode the URL using the old decoding
// strategy if the URL could not be inflated.
try {
decoded = decodeURIComponent(atob(input));
} catch (error) {
return {};
}
}

if (!/\d\d.*/.test(decoded)) return {};
Expand Down

0 comments on commit cca726a

Please sign in to comment.