Skip to content

Commit 0ef99e3

Browse files
committed
refactor: remove lz-string and raw base64, unify on gzip compression
- Remove lz-string dependency (was used for ?s= share URLs) - Remove ?r= raw base64url encoding (no compression, 33% larger) - Unify all URL sharing on ?c= (gzip + base64url) - createShareUrl now uses browser-native CompressionStream - Share button, import hook, and skill all use the same ?c= format - Zero external dependencies for compression/decompression https://claude.ai/code/session_01Cv3BVrC7kNVEtijCfe2R5e
1 parent ccb0e81 commit 0ef99e3

8 files changed

Lines changed: 52 additions & 98 deletions

File tree

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,8 @@ Ever struggled with deeply nested, escaped JSON strings? Like this nightmare:
7575

7676
Share JSON instantly via URL — no server required:
7777

78-
- **Share Button** - One-click share with LZ-String compression (`?s=` parameter)
78+
- **Share Button** - One-click share with gzip compression (`?c=` parameter)
7979
- **Custom Tab Names** - Hover "Share" to name your tab before sharing (`?t=` parameter)
80-
- **Base64url Mode** - Shell-friendly encoding without dependencies (`?r=` parameter)
8180
- **Hero Direct Link** - Add `?h=1` to auto-open JSON Hero viewer on import
8281

8382
### 🤖 Claude Code Skill: `present-json`
@@ -105,9 +104,7 @@ echo "https://hrhrng.github.io/super-json?c=${encoded}&t=API+Response&h=1"
105104

106105
| Param | Description | Example |
107106
|-------|-------------|---------|
108-
| `c` | Gzip + Base64url compressed JSON (recommended) | `?c=H4sIA...` |
109-
| `s` | LZ-String compressed JSON (shorter URLs) | `?s=NoIgbg9...` |
110-
| `r` | Base64url encoded JSON (uncompressed fallback) | `?r=eyJrZXki...` |
107+
| `c` | Gzip + Base64url compressed JSON | `?c=H4sIA...` |
111108
| `t` | Custom tab name | `&t=My+Results` |
112109
| `h` | Auto-switch to Hero mode | `&h=1` |
113110

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"@monaco-editor/react": "^4.6.0",
2525
"clsx": "^2.1.1",
2626
"immer": "^10.1.1",
27-
"lz-string": "^1.5.0",
2827
"nanoid": "^5.0.9",
2928
"react": "^18.3.1",
3029
"react-dom": "^18.3.1",

skills/present-json/SKILL.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ bash scripts/present.sh '{"key": "value"}' --tab "My Data" --hero
7373

7474
| Parameter | Encoding | Description |
7575
|-----------|----------|-------------|
76-
| `c` | Gzip + Base64url | **Recommended** — compressed, shell-friendly, shortest URLs |
77-
| `s` | LZ-String compressed | Used by the app's built-in Share button |
78-
| `r` | Base64url | Uncompressed fallback, shell-friendly |
79-
| `t` | URL-encoded string | Custom tab name (works with `c`, `s`, and `r`) |
76+
| `c` | Gzip + Base64url | Compressed JSON data |
77+
| `t` | URL-encoded string | Custom tab name |
8078
| `h` | `1` to enable | Auto-switch to Hero mode and load JSON Hero viewer |

src/components/ShareButton/ShareButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'
22
import { createPortal } from 'react-dom'
33
import { createShareUrl, copyToClipboard } from '@utils/simpleShare'
44

5+
56
interface ShareButtonProps {
67
getContent: () => string | undefined
78
onNotification: (type: 'success' | 'error', message: string) => void
@@ -46,7 +47,7 @@ export function ShareButton({ getContent, onNotification }: ShareButtonProps) {
4647

4748
setSharing(true)
4849
try {
49-
const result = createShareUrl(content, customTabName || undefined)
50+
const result = await createShareUrl(content, customTabName || undefined)
5051
await copyToClipboard(result.url)
5152
onNotification('success', `Share link copied! (${result.length} chars)`)
5253
} catch (error) {

src/hooks/useSimpleImport.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useRef } from 'react'
22
import { useDocumentStore } from '@stores/documentStore'
33
import { useAppStore } from '@stores/appStore'
4-
import { importFromUrl, importFromBase64Url, importFromCompressedUrl } from '@utils/simpleShare'
4+
import { importFromCompressedUrl } from '@utils/simpleShare'
55
import { useNotification } from '@components/Notification/Notification'
66

77
// Track if import has been processed globally to prevent duplicates
@@ -17,14 +17,12 @@ export function useSimpleImport() {
1717
const handleImport = async () => {
1818
// Check URL parameters for shared data
1919
const urlParams = new URLSearchParams(window.location.search)
20-
const compressedData = urlParams.get('s') // 's' for share (LZ-String compressed)
21-
const rawBase64Data = urlParams.get('r') // 'r' for raw (base64url encoded)
2220
const gzipBase64Data = urlParams.get('c') // 'c' for compressed (gzip + base64url)
2321
const tabName = urlParams.get('t') // 't' for tab name
2422
const heroMode = urlParams.get('h') // 'h' for hero mode (auto load → hero)
2523

2624
// Check both local ref and global flag to prevent duplicates
27-
if ((!compressedData && !rawBase64Data && !gzipBase64Data) || hasImportedRef.current || isImportProcessed) return
25+
if (!gzipBase64Data || hasImportedRef.current || isImportProcessed) return
2826

2927
hasImportedRef.current = true
3028
isImportProcessed = true
@@ -38,11 +36,7 @@ export function useSimpleImport() {
3836
message: 'Importing shared content to new tab...'
3937
})
4038

41-
const inputContent = compressedData
42-
? importFromUrl(compressedData)
43-
: gzipBase64Data
44-
? await importFromCompressedUrl(gzipBase64Data)
45-
: importFromBase64Url(rawBase64Data!)
39+
const inputContent = await importFromCompressedUrl(gzipBase64Data)
4640

4741
// Create a new document with the imported content
4842
const docId = createDocument()
@@ -100,8 +94,6 @@ export function useSimpleImport() {
10094

10195
// Clean up the URL
10296
const newUrl = new URL(window.location.href)
103-
newUrl.searchParams.delete('s')
104-
newUrl.searchParams.delete('r')
10597
newUrl.searchParams.delete('c')
10698
newUrl.searchParams.delete('t')
10799
newUrl.searchParams.delete('h')

src/utils/__tests__/simpleShare.test.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,8 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest'
2-
import { importFromBase64Url, importFromCompressedUrl } from '../simpleShare'
2+
import { importFromCompressedUrl } from '../simpleShare'
33
import { gzipSync } from 'node:zlib'
44

55
describe('simpleShare', () => {
6-
describe('importFromBase64Url', () => {
7-
it('should decode base64url-encoded JSON', () => {
8-
// echo -n '{"name":"test"}' | base64 | tr '+/' '-_' | tr -d '='
9-
const encoded = btoa('{"name":"test"}').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
10-
const result = importFromBase64Url(encoded)
11-
expect(result).toBe('{"name":"test"}')
12-
})
13-
14-
it('should handle base64url with padding stripped', () => {
15-
// Content that would normally need padding
16-
const input = '{"a":1}'
17-
const encoded = btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
18-
const result = importFromBase64Url(encoded)
19-
expect(result).toBe(input)
20-
})
21-
22-
it('should handle unicode content', () => {
23-
const input = '{"message":"hello"}'
24-
const encoded = btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
25-
const result = importFromBase64Url(encoded)
26-
expect(result).toBe(input)
27-
})
28-
29-
it('should throw on invalid base64 data', () => {
30-
expect(() => importFromBase64Url('!!!invalid!!!')).toThrow('Failed to import shared content')
31-
})
32-
})
33-
346
describe('importFromCompressedUrl', () => {
357
// Helper: gzip + base64url encode (mirrors the shell command)
368
function gzipBase64Url(input: string): string {

src/utils/simpleShare.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,45 @@
1-
import LZString from 'lz-string'
1+
async function gzipCompress(input: string): Promise<Uint8Array> {
2+
const encoder = new TextEncoder()
3+
const data = encoder.encode(input)
4+
5+
const cs = new CompressionStream('gzip')
6+
const writer = cs.writable.getWriter()
7+
writer.write(data)
8+
writer.close()
9+
10+
const reader = cs.readable.getReader()
11+
const chunks: Uint8Array[] = []
12+
while (true) {
13+
const { done, value } = await reader.read()
14+
if (done) break
15+
chunks.push(value)
16+
}
217

3-
export function createShareUrl(inputContent: string, tabName?: string): { url: string; length: number } {
4-
// Compress only the input content
5-
const compressed = LZString.compressToEncodedURIComponent(inputContent)
18+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
19+
const result = new Uint8Array(totalLength)
20+
let offset = 0
21+
for (const chunk of chunks) {
22+
result.set(chunk, offset)
23+
offset += chunk.length
24+
}
25+
return result
26+
}
27+
28+
function toBase64Url(bytes: Uint8Array): string {
29+
let binary = ''
30+
for (let i = 0; i < bytes.length; i++) {
31+
binary += String.fromCharCode(bytes[i])
32+
}
33+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
34+
}
35+
36+
export async function createShareUrl(inputContent: string, tabName?: string): Promise<{ url: string; length: number }> {
37+
const compressed = await gzipCompress(inputContent)
38+
const encoded = toBase64Url(compressed)
639

7-
// Create share URL
840
const baseUrl = window.location.origin + window.location.pathname
9-
let shareUrl = `${baseUrl}?s=${compressed}`
41+
let shareUrl = `${baseUrl}?c=${encoded}`
1042

11-
// Add tab name parameter if provided
1243
if (tabName) {
1344
shareUrl += `&t=${encodeURIComponent(tabName)}`
1445
}
@@ -19,43 +50,6 @@ export function createShareUrl(inputContent: string, tabName?: string): { url: s
1950
}
2051
}
2152

22-
export function importFromUrl(compressedData: string): string {
23-
try {
24-
// Decompress the data
25-
const inputContent = LZString.decompressFromEncodedURIComponent(compressedData)
26-
27-
if (!inputContent) {
28-
throw new Error('Invalid share link: Unable to decompress data')
29-
}
30-
31-
return inputContent
32-
} catch (error) {
33-
console.error('Error importing from URL:', error)
34-
throw new Error('Failed to import shared content. Please check the link and try again.')
35-
}
36-
}
37-
38-
export function importFromBase64Url(base64Data: string): string {
39-
try {
40-
// Convert base64url to standard base64
41-
let base64 = base64Data.replace(/-/g, '+').replace(/_/g, '/')
42-
// Add padding if needed
43-
while (base64.length % 4 !== 0) {
44-
base64 += '='
45-
}
46-
const inputContent = decodeURIComponent(escape(atob(base64)))
47-
48-
if (!inputContent) {
49-
throw new Error('Invalid share link: Unable to decode data')
50-
}
51-
52-
return inputContent
53-
} catch (error) {
54-
console.error('Error importing from base64 URL:', error)
55-
throw new Error('Failed to import shared content. Please check the link and try again.')
56-
}
57-
}
58-
5953
export async function importFromCompressedUrl(compressedData: string): Promise<string> {
6054
try {
6155
// Convert base64url to standard base64
@@ -120,7 +114,7 @@ export function copyToClipboard(text: string): Promise<void> {
120114
document.body.appendChild(textArea)
121115
textArea.focus()
122116
textArea.select()
123-
117+
124118
try {
125119
document.execCommand('copy')
126120
resolve()
@@ -131,4 +125,4 @@ export function copyToClipboard(text: string): Promise<void> {
131125
}
132126
})
133127
}
134-
}
128+
}

0 commit comments

Comments
 (0)