Skip to content

Commit 5cfe36e

Browse files
committed
close #15: implement shareable URLs
1 parent 7a479ec commit 5cfe36e

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

code.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@
7474
});
7575
}
7676

77+
function resetLoadingState() {
78+
promptText.style.display = 'block';
79+
toolbar.style.display = 'none';
80+
statusBar.style.display = 'none';
81+
canvas.style.display = 'none';
82+
}
83+
7784
function showLoadingError(text) {
7885
promptText.style.display = 'block';
7986
toolbar.style.display = 'none';
@@ -465,6 +472,7 @@
465472
toolbar.style.display = 'flex';
466473
statusBar.style.display = 'flex';
467474
canvas.style.display = 'block';
475+
updateHash(code, map);
468476
const sm = parseSourceMap(map);
469477

470478
// Populate the file picker
@@ -1531,6 +1539,87 @@
15311539
// Older browsers
15321540
darkMedia.addListener(onDarkModeChange)
15331541
}
1542+
1543+
////////////////////////////////////////////////////////////////////////////////
1544+
// Shareable URLs
1545+
1546+
const utf8ToUTF16 = x => decodeURIComponent(escape(x));
1547+
const utf16ToUTF8 = x => unescape(encodeURIComponent(x));
1548+
1549+
function loadFromHash() {
1550+
try {
1551+
// Reads a string in length-prefix form separated by a null character. This
1552+
// format is used because it's simple and also more compact than JSON.
1553+
const readBuffer = () => {
1554+
const zero = hash.indexOf('\0');
1555+
if (zero < 0) throw 'No null character';
1556+
const start = zero + 1;
1557+
const end = start + (0 | hash.slice(0, zero));
1558+
const buffer = hash.slice(start, end);
1559+
if (end > hash.length) throw 'Invalid length';
1560+
hash = hash.slice(end);
1561+
return buffer;
1562+
};
1563+
1564+
// Extract the length-prefixed data
1565+
let hash = atob(location.hash.slice(1));
1566+
const code = readBuffer();
1567+
const map = readBuffer();
1568+
if (hash !== '') throw 'Unexpected extra data';
1569+
1570+
finishLoading(utf8ToUTF16(code), utf8ToUTF16(map));
1571+
} catch (e) {
1572+
// Clear out an invalid hash and reset the UI
1573+
if (location.hash !== '') {
1574+
try {
1575+
history.replaceState({}, '', location.pathname);
1576+
} catch (e) {
1577+
}
1578+
}
1579+
resetLoadingState();
1580+
}
1581+
}
1582+
1583+
function updateHash(code, map) {
1584+
try {
1585+
const btoaLength = n => 4 * ((n + 2) / 3 | 0)
1586+
const kMaxURLDisplayChars = 32 * 1024; // Chrome limits URLs to 32kb in size
1587+
const url = new URL(location.href);
1588+
url.hash = '#'; // Clear the data in the hash but leave the hash prefix
1589+
const urlLength = url.href.length;
1590+
1591+
// Do a cheap check to see if the URL will be too long
1592+
let codeLength = `${code.length}\0`;
1593+
let mapLength = `${map.length}\0`;
1594+
let finalLength = urlLength + btoaLength(codeLength.length + code.length + mapLength.length + map.length)
1595+
if (finalLength >= kMaxURLDisplayChars) throw 'URL estimate too long';
1596+
1597+
// Do the expensive check to see if the URL will be too long
1598+
code = utf16ToUTF8(code);
1599+
map = utf16ToUTF8(map);
1600+
codeLength = `${code.length}\0`;
1601+
mapLength = `${map.length}\0`;
1602+
finalLength = urlLength + btoaLength(codeLength.length + code.length + mapLength.length + map.length)
1603+
if (finalLength >= kMaxURLDisplayChars) throw 'URL too long';
1604+
1605+
// Only pay the cost of building the string now that we know it'll work
1606+
const hash = '#' + btoa(`${codeLength}${code}${mapLength}${map}`);
1607+
if (location.hash !== hash) {
1608+
history.pushState({}, '', hash);
1609+
}
1610+
} catch (e) {
1611+
// Push an empty hash instead if it's too big for a URL
1612+
if (location.hash !== '') {
1613+
try {
1614+
history.pushState({}, '', location.pathname);
1615+
} catch (e) {
1616+
}
1617+
}
1618+
}
1619+
}
1620+
1621+
loadFromHash();
1622+
addEventListener('popstate', () => loadFromHash());
15341623
})();
15351624

15361625
const exampleJS = `// Generated by CoffeeScript 2.5.1

0 commit comments

Comments
 (0)