Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions package-lock.json

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

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"dependencies": {
"ansi-to-react": "^6.2.6",
"bootstrap": "^5.3.2",
"lz-string": "^1.5.0",
"pretty-ms": "^8.0.0",
"react": "^18.2.0",
"react-ace": "^10.1.0",
Expand Down
62 changes: 33 additions & 29 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './App.css'

import ReconnectingWebSocket from 'reconnecting-websocket'

import LZString from 'lz-string'
import { decompressFromURL } from './compression'

import Menu from './Menu'
import LeftPane from './LeftPane'
Expand Down Expand Up @@ -31,37 +31,13 @@ def main(): Unit \\\\ IO =
println(area(Shape.Rectangle(2, 4)))
`

function getInitialProgram() {
// Decode the initial program from the query param (if available).
let urlParams = new URLSearchParams(window.location.search)
let qparam = urlParams.get('q')
if (typeof qparam === 'string' && qparam.length > 0) {
console.log('Using initial program from query parameter.')
return LZString.decompressFromEncodedURIComponent(qparam)
}

// Otherwise, retrieve the initial program from local storage (if available).
let storedProgram = localStorage.getItem('program')
if (typeof storedProgram === 'string') {
console.log('Using initial program from local storage.')
return storedProgram
}

// Otherwise use the default program.
console.log('Using default initial program.')
return defaultProgram
}

export default function App() {
const initialProgram = useRef(getInitialProgram()).current

const [connected, setConnected] = useState(undefined)
const [program, setProgram] = useState(initialProgram)
const [program, setProgram] = useState(null)
const [result, setResult] = useState('')
const [version, setVersion] = useState(undefined)
const [compilationTime, setCompilationTime] = useState(undefined)
const [evaluationTime, setEvaluationTime] = useState(undefined)
const [url, setUrl] = useState('?q=' + LZString.compressToEncodedURIComponent(initialProgram))

const websocket = useRef(null)
const programRef = useRef(program)
Expand All @@ -71,6 +47,31 @@ export default function App() {
programRef.current = program
connectedRef.current = connected

// Load initial program (async for URL decompression)
useEffect(() => {
async function loadInitialProgram() {
const urlParams = new URLSearchParams(window.location.search)
const qparam = urlParams.get('q')
if (typeof qparam === 'string' && qparam.length > 0) {
console.log('Using initial program from query parameter.')
const decoded = await decompressFromURL(qparam)
setProgram(decoded)
return
}

const storedProgram = localStorage.getItem('program')
if (typeof storedProgram === 'string') {
console.log('Using initial program from local storage.')
setProgram(storedProgram)
return
}

console.log('Using default initial program.')
setProgram(defaultProgram)
}
loadInitialProgram()
}, [])

useEffect(() => {
let options = {
connectionTimeout: 2500,
Expand Down Expand Up @@ -103,9 +104,10 @@ export default function App() {

const notifyOnChange = useCallback(src => {
localStorage.setItem('program', src)
let qparam = LZString.compressToEncodedURIComponent(src)
setProgram(src)
setUrl('?q=' + qparam)
if (window.location.search) {
window.history.replaceState(undefined, undefined, window.location.pathname)
}
}, [])

const notifyRun = useCallback(() => {
Expand Down Expand Up @@ -140,9 +142,11 @@ export default function App() {
websocket.current.send(JSON.stringify(data))
}, [])

if (program === null) return null

return (
<div>
<Menu connected={connected} notifyRun={notifyRun} notifySampleChange={notifyOnChange} url={url} />
<Menu connected={connected} notifyRun={notifyRun} notifySampleChange={notifyOnChange} program={program} />
<div className="page">
<LeftPane initial={program} notifyOnChange={notifyOnChange} />
<RightPane
Expand Down
6 changes: 4 additions & 2 deletions src/Menu.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState } from 'react'
import { Button, Form } from 'reactstrap'
import SamplesData from './data/Samples'
import { compressToURL } from './compression'

export default function Menu({ connected, notifyRun, notifySampleChange, url }) {
export default function Menu({ connected, notifyRun, notifySampleChange, program }) {
const [choice, setChoice] = useState(undefined)

function getRunButton() {
Expand Down Expand Up @@ -78,7 +79,8 @@ export default function Menu({ connected, notifyRun, notifySampleChange, url })
notifySampleChange(SamplesData[newChoice].code)
}

function updateLinkUrl() {
async function updateLinkUrl() {
const url = await compressToURL(program)
window.history.pushState(undefined, undefined, url)
}

Expand Down
38 changes: 38 additions & 0 deletions src/compression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Compression helpers using the native CompressionStream API.
* Produces base64url-encoded DEFLATE-raw payloads for URL sharing.
*/

/**
* Compress a string and return a URL query string like `?q=...`.
* @param {string} text
* @returns {Promise<string>}
*/
export async function compressToURL(text) {
const bytes = new TextEncoder().encode(text)
const cs = new CompressionStream('deflate-raw')
const writer = cs.writable.getWriter()
writer.write(bytes)
writer.close()
const compressed = await new Response(cs.readable).arrayBuffer()
const base64 = btoa(String.fromCharCode(...new Uint8Array(compressed)))
const base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
return '?q=' + base64url
}

/**
* Decompress a base64url-encoded DEFLATE-raw payload back to a string.
* @param {string} param - the raw value of the `q` query parameter
* @returns {Promise<string>}
*/
export async function decompressFromURL(param) {
const base64 = param.replace(/-/g, '+').replace(/_/g, '/')
const binary = atob(base64)
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0))
const ds = new DecompressionStream('deflate-raw')
const writer = ds.writable.getWriter()
writer.write(bytes)
writer.close()
const decompressed = await new Response(ds.readable).arrayBuffer()
return new TextDecoder().decode(decompressed)
}
Loading