1+ import 'core-js/modules/es.uint8-array.from-base64' ;
2+ import 'core-js/modules/es.uint8-array.to-base64' ;
3+
14import { createRoot } from 'react-dom/client' ;
25
36import * as React from 'react' ;
@@ -13,6 +16,7 @@ import IconButton from '@mui/joy/IconButton';
1316import Select from '@mui/joy/Select' ;
1417import Option from '@mui/joy/Option' ;
1518import Link from '@mui/joy/Link' ;
19+ import CircularProgress from '@mui/joy/CircularProgress' ;
1620import Snackbar from '@mui/joy/Snackbar' ;
1721import Alert from '@mui/joy/Alert' ;
1822import Tabs from '@mui/joy/Tabs' ;
@@ -30,26 +34,90 @@ import * as monaco from 'monaco-editor';
3034import { EditorState , Editor } from './monaco' ;
3135import { Viewer as WaveformViewer } from './d3wave' ;
3236import { PythonError , runner } from './runner' ;
37+ import { compress , decompress } from './zstd' ;
3338import data from './config' ;
3439
3540import './app.css' ;
3641
37- function stealHashQuery ( ) {
42+ interface SharePayload {
43+ av : string ;
44+ s : string ;
45+ }
46+
47+ const zstdMagic = new Uint8Array ( [ 0x28 , 0xb5 , 0x2f , 0xfd ] ) ;
48+
49+ const getSharePayloadDictionary = ( ( ) => {
50+ let promise : Promise < Uint8Array > | undefined ;
51+ return async ( ) => {
52+ return promise ??=
53+ fetch ( new URL ( 'zstd/dictionary.bin' , import . meta. url ) . toString ( ) )
54+ . then ( ( response ) => response . arrayBuffer ( ) )
55+ . then ( ( buffer ) => new Uint8Array ( buffer ) ) ;
56+ } ;
57+ } ) ( ) ;
58+
59+ async function createShareFragment ( sourceCode : string , amaranthVersion : string ) : Promise < string > {
60+ let payload = JSON . stringify ( {
61+ av : amaranthVersion ,
62+ s : sourceCode ,
63+ } satisfies SharePayload ) ;
64+
65+ let compressed = await compress (
66+ new TextEncoder ( ) . encode ( payload ) ,
67+ getSharePayloadDictionary ( ) ,
68+ ) ;
69+
70+ // Remove the Zstandard magic bytes and add a version field
71+ let result = new Uint8Array ( 1 + compressed . length - zstdMagic . length ) ;
72+ result [ 0 ] = 1 ; // version
73+ result . set ( compressed . subarray ( zstdMagic . length ) , 1 ) ;
74+
75+ return result . toBase64 ( ) ;
76+ }
77+
78+ async function decodeShareFragment ( hashQuery : string ) : Promise < SharePayload > {
79+ if ( / ^ [ 0 - 9 A - Z a - z + / = ] + $ / . test ( hashQuery ) ) {
80+ let bytes = Uint8Array . fromBase64 ( hashQuery ) ;
81+ // Check the version
82+ if ( bytes [ 0 ] === 1 ) {
83+ let zstdPayload = bytes . subarray ( 1 ) ;
84+ if ( indexedDB . cmp ( bytes . subarray ( 0 , 4 ) , zstdMagic ) !== 0 ) {
85+ zstdPayload = ( ( ) => {
86+ let result = new Uint8Array ( zstdMagic . length + zstdPayload . length ) ;
87+ result . set ( zstdMagic ) ;
88+ result . set ( zstdPayload , zstdMagic . length ) ;
89+ return result ;
90+ } ) ( ) ;
91+ }
92+ bytes = await decompress (
93+ zstdPayload ,
94+ getSharePayloadDictionary ( ) ,
95+ ) ;
96+ } else if ( bytes [ 0 ] !== '{' . charCodeAt ( 0 ) ) {
97+ return ;
98+ }
99+ return JSON . parse ( new TextDecoder ( ) . decode ( bytes ) ) ;
100+ } else {
101+ // Legacy encoding, used 2024-02-16 to 2024-02-24.
102+ return JSON . parse ( decodeURIComponent ( hashQuery . replace ( '+' , '%20' ) ) ) ;
103+ }
104+ }
105+
106+ async function stealHashQuery ( ) {
38107 const { hash } = window . location ;
39108 if ( hash !== '' ) {
40109 history . replaceState ( null , '' , ' ' ) ; // remove #... from URL entirely
41110 const hashQuery = hash . substring ( 1 ) ;
42111 try {
43- return JSON . parse ( atob ( hashQuery ) ) ;
44- } catch {
45- try {
46- // Legacy encoding, used 2024-02-16 to 2024-02-24.
47- return JSON . parse ( decodeURIComponent ( hashQuery . replace ( '+' , '%20' ) ) ) ;
48- } catch { }
112+ return decodeShareFragment ( hashQuery ) ;
113+ } catch ( error ) {
114+ console . warn ( 'Could not parse the URL fragment, ignoring.' , error ) ;
49115 }
50116 }
51117}
52118
119+ const query : { av ?: string , s ?: string } | undefined = await stealHashQuery ( ) ;
120+
53121interface TerminalChunk {
54122 stream : 'stdout' | 'stderr' ;
55123 text : string ;
@@ -64,14 +132,14 @@ function AppContent() {
64132 const { mode, setMode} = useColorScheme ( ) ;
65133 useEffect ( ( ) => monaco . editor . setTheme ( mode === 'light' ? 'vs' : 'vs-dark' ) , [ mode ] ) ;
66134
67- const query : { av ?: string , s ?: string } | undefined = stealHashQuery ( ) ;
68135 const [ amaranthVersion , setAmaranthVersion ] = useState (
69136 query ?. av
70137 ?? localStorage . getItem ( 'amaranth-playground.amaranthVersion' )
71138 ?? data . amaranthVersions [ 0 ] ) ;
72139 useEffect ( ( ) => localStorage . setItem ( 'amaranth-playground.amaranthVersion' , amaranthVersion ) , [ amaranthVersion ] ) ;
73140 const [ running , setRunning ] = useState ( false ) ;
74141 const [ sharingOpen , setSharingOpen ] = useState ( false ) ;
142+ const [ shareURL , setShareURL ] = useState ( '' ) ;
75143 const [ tutorialDone , setTutorialDone ] = useState ( localStorage . getItem ( 'amaranth-playground.tutorialDone' ) !== null ) ;
76144 useEffect ( ( ) => tutorialDone ? localStorage . setItem ( 'amaranth-playground.tutorialDone' , '' ) : void 0 , [ tutorialDone ] ) ;
77145 const [ activeTab , setActiveTab ] = useState ( tutorialDone ? 'amaranth-source' : 'tutorial' ) ;
@@ -160,6 +228,14 @@ function AppContent() {
160228 const runCodeRef = useRef ( runCode ) ;
161229 runCodeRef . current = runCode ;
162230
231+ async function shareCode ( ) {
232+ setSharingOpen ( true ) ;
233+ setShareURL ( '' ) ;
234+ let fragment = await createShareFragment ( amaranthSource , amaranthVersion ) ;
235+ let url = new URL ( '#' + fragment , window . location . href ) . toString ( ) ;
236+ setShareURL ( url ) ;
237+ }
238+
163239 const amaranthSourceEditorActions = React . useMemo ( ( ) => [
164240 {
165241 id : 'amaranth-playground.run' ,
@@ -472,7 +548,7 @@ function AppContent() {
472548 color = 'neutral'
473549 variant = 'outlined'
474550 endDecorator = { < ShareIcon /> }
475- onClick = { ( ) => setSharingOpen ( true ) }
551+ onClick = { ( ) => shareCode ( ) }
476552 >
477553 Share
478554 </ Button >
@@ -482,14 +558,11 @@ function AppContent() {
482558 open = { sharingOpen }
483559 onClose = { ( _event , _reason ) => setSharingOpen ( false ) }
484560 >
485- < Link href = {
486- // base64 overhead is fixed at 33%, urlencode overhead is variable, typ. 133% (!)
487- new URL ( '#' + btoa ( JSON . stringify ( {
488- av : amaranthVersion , s : amaranthSource
489- } ) ) , window . location . href ) . toString ( )
490- } >
491- Copy this link to share the source code
492- </ Link >
561+ { shareURL === '' ? < CircularProgress /> : (
562+ < Link href = { shareURL } >
563+ Copy this link to share the source code
564+ </ Link >
565+ ) }
493566 </ Snackbar >
494567
495568 < IconButton
0 commit comments