Skip to content

Commit 64fe674

Browse files
PlayBridgeJS: Add code sharing feature with URL parameter compression
- Add compression-based code sharing functionality using CompressionStream API - Implement encoding type versioning (enc parameter) for future extensibility - Add share button with modal dialog and copy-to-clipboard functionality - Support automatic URL parameter detection on page load - Use gzip compression with base64 encoding for URL-safe sharing - Responsive design with mobile support
1 parent d2a2ca2 commit 64fe674

File tree

5 files changed

+421
-16
lines changed

5 files changed

+421
-16
lines changed

Examples/PlayBridgeJS/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from
44

55
```sh
66
$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasi.artifactbundle.zip
7-
$ ./build.sh
7+
$ ./build.sh release
88
$ npx serve --symlinks
99
```

Examples/PlayBridgeJS/Sources/JavaScript/app.js

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { EditorSystem } from './editor.js';
33
import ts from 'typescript';
44
import { TypeProcessor } from './processor.js';
5+
import { CodeShareManager } from './code-share.js';
56

67
/**
78
* @typedef {import('../../.build/plugins/PackageToJS/outputs/Package/bridge-js.js').PlayBridgeJS} PlayBridgeJS
@@ -23,19 +24,20 @@ export class BridgeJSPlayground {
2324
/** @type {boolean} */
2425
this.isInitialized = false;
2526

26-
const errorDisplay = document.getElementById('errorDisplay');
27-
if (!errorDisplay) {
28-
throw new Error('Error display element not found');
29-
}
30-
/** @type {HTMLElement} */
31-
this.errorDisplay = errorDisplay;
32-
33-
const errorMessage = document.getElementById('errorMessage');
34-
if (!errorMessage) {
35-
throw new Error('Error message element not found');
36-
}
37-
/** @type {HTMLElement} */
38-
this.errorMessage = errorMessage;
27+
/** @type {HTMLDivElement} */
28+
this.errorDisplay = /** @type {HTMLDivElement} */ (document.getElementById('errorDisplay'));
29+
/** @type {HTMLDivElement} */
30+
this.errorMessage = /** @type {HTMLDivElement} */ (document.getElementById('errorMessage'));
31+
/** @type {HTMLButtonElement} */
32+
this.shareButton = /** @type {HTMLButtonElement} */ (document.getElementById('shareButton'));
33+
/** @type {HTMLDialogElement} */
34+
this.shareDialog = /** @type {HTMLDialogElement} */ (document.getElementById('shareDialog'));
35+
/** @type {HTMLInputElement} */
36+
this.shareUrlInput = /** @type {HTMLInputElement} */ (document.getElementById('shareUrl'));
37+
/** @type {HTMLButtonElement} */
38+
this.copyButton = /** @type {HTMLButtonElement} */ (document.getElementById('copyButton'));
39+
/** @type {HTMLButtonElement} */
40+
this.closeShareDialogButton = /** @type {HTMLButtonElement} */ (document.getElementById('closeShareDialog'));
3941
}
4042

4143
/**
@@ -57,8 +59,14 @@ export class BridgeJSPlayground {
5759
// Set up event listeners
5860
this.setupEventListeners();
5961

60-
// Load sample code
61-
this.editorSystem.setInputs(sampleCode)
62+
// Check for shared code in URL
63+
const sharedCode = await CodeShareManager.extractCodeFromUrl();
64+
if (sharedCode) {
65+
this.editorSystem.setInputs(sharedCode);
66+
} else {
67+
// Load sample code
68+
this.editorSystem.setInputs(sampleCode);
69+
}
6270

6371
this.isInitialized = true;
6472
console.log('BridgeJS Playground initialized successfully');
@@ -98,6 +106,69 @@ export class BridgeJSPlayground {
98106
}
99107
this.generateTimeout = setTimeout(() => this.generateCode(), 300);
100108
});
109+
110+
// Set up share functionality
111+
this.setupShareListeners();
112+
}
113+
114+
// Set up share-related event listeners
115+
setupShareListeners() {
116+
// Show share dialog
117+
this.shareButton.addEventListener('click', async () => {
118+
try {
119+
const inputs = this.editorSystem.getInputs();
120+
const shareUrl = await CodeShareManager.generateShareUrl(inputs);
121+
this.shareUrlInput.value = shareUrl;
122+
this.shareDialog.classList.remove('hidden');
123+
this.shareUrlInput.select();
124+
} catch (error) {
125+
console.error('Failed to generate share URL:', error);
126+
this.showError('Failed to generate share URL: ' + error.message);
127+
}
128+
});
129+
130+
// Copy share URL
131+
this.copyButton.addEventListener('click', async () => {
132+
try {
133+
await navigator.clipboard.writeText(this.shareUrlInput.value);
134+
135+
const originalText = this.copyButton.textContent;
136+
this.copyButton.textContent = 'Copied!';
137+
this.copyButton.classList.add('copied');
138+
139+
setTimeout(() => {
140+
this.copyButton.textContent = originalText;
141+
this.copyButton.classList.remove('copied');
142+
}, 2000);
143+
} catch (error) {
144+
console.error('Failed to copy URL:', error);
145+
this.shareUrlInput.select();
146+
}
147+
});
148+
149+
// Close share dialog
150+
this.closeShareDialogButton.addEventListener('click', () => {
151+
this.shareDialog.classList.add('hidden');
152+
});
153+
154+
// Close dialog when clicking outside
155+
document.addEventListener('click', (event) => {
156+
if (!this.shareDialog.classList.contains('hidden')) {
157+
const dialogContent = this.shareDialog.querySelector('.share-dialog-content');
158+
const target = event.target;
159+
if (dialogContent && target instanceof Node && !dialogContent.contains(target) &&
160+
this.shareButton && !this.shareButton.contains(target)) {
161+
this.shareDialog.classList.add('hidden');
162+
}
163+
}
164+
});
165+
166+
// Close dialog with Escape key
167+
document.addEventListener('keydown', (event) => {
168+
if (event.key === 'Escape' && !this.shareDialog.classList.contains('hidden')) {
169+
this.shareDialog.classList.add('hidden');
170+
}
171+
});
101172
}
102173

103174
createTS2Skeleton() {
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// @ts-check
2+
3+
export class CodeCompression {
4+
/**
5+
* Compresses a string using gzip compression and returns base64-encoded result.
6+
* @param {string} text - The text to compress
7+
* @returns {Promise<string>} Base64-encoded compressed string
8+
*/
9+
static async compress(text) {
10+
const textEncoder = new TextEncoder();
11+
const stream = new CompressionStream('gzip');
12+
const writer = stream.writable.getWriter();
13+
const reader = stream.readable.getReader();
14+
15+
// Start compression
16+
const writePromise = writer.write(textEncoder.encode(text)).then(() => writer.close());
17+
18+
// Read compressed chunks
19+
const chunks = [];
20+
let readResult;
21+
do {
22+
readResult = await reader.read();
23+
if (readResult.value) {
24+
chunks.push(readResult.value);
25+
}
26+
} while (!readResult.done);
27+
28+
await writePromise;
29+
30+
// Combine all chunks into single Uint8Array
31+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
32+
const compressed = new Uint8Array(totalLength);
33+
let offset = 0;
34+
for (const chunk of chunks) {
35+
compressed.set(chunk, offset);
36+
offset += chunk.length;
37+
}
38+
39+
// Convert to base64 for URL safety
40+
return this.uint8ArrayToBase64(compressed);
41+
}
42+
43+
/**
44+
* Decompresses a base64-encoded gzip string back to original text.
45+
* @param {string} compressedBase64 - Base64-encoded compressed string
46+
* @returns {Promise<string>} Original decompressed text
47+
*/
48+
static async decompress(compressedBase64) {
49+
const compressed = this.base64ToUint8Array(compressedBase64);
50+
const stream = new DecompressionStream('gzip');
51+
const writer = stream.writable.getWriter();
52+
const reader = stream.readable.getReader();
53+
54+
// Start decompression
55+
const writePromise = writer.write(compressed).then(() => writer.close());
56+
57+
// Read decompressed chunks
58+
const chunks = [];
59+
let readResult;
60+
do {
61+
readResult = await reader.read();
62+
if (readResult.value) {
63+
chunks.push(readResult.value);
64+
}
65+
} while (!readResult.done);
66+
67+
await writePromise;
68+
69+
// Combine chunks and decode
70+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
71+
const decompressed = new Uint8Array(totalLength);
72+
let offset = 0;
73+
for (const chunk of chunks) {
74+
decompressed.set(chunk, offset);
75+
offset += chunk.length;
76+
}
77+
78+
const textDecoder = new TextDecoder();
79+
return textDecoder.decode(decompressed);
80+
}
81+
82+
/**
83+
* Converts Uint8Array to base64 string.
84+
* @param {Uint8Array} uint8Array - Array to convert
85+
* @returns {string} Base64 string
86+
*/
87+
static uint8ArrayToBase64(uint8Array) {
88+
let binary = '';
89+
for (let i = 0; i < uint8Array.byteLength; i++) {
90+
binary += String.fromCharCode(uint8Array[i]);
91+
}
92+
return btoa(binary);
93+
}
94+
95+
/**
96+
* Converts base64 string to Uint8Array.
97+
* @param {string} base64 - Base64 string to convert
98+
* @returns {Uint8Array} Converted array
99+
*/
100+
static base64ToUint8Array(base64) {
101+
const binary = atob(base64);
102+
const bytes = new Uint8Array(binary.length);
103+
for (let i = 0; i < binary.length; i++) {
104+
bytes[i] = binary.charCodeAt(i);
105+
}
106+
return bytes;
107+
}
108+
}
109+
110+
/**
111+
* URL parameter manager for sharing code.
112+
* Handles compression, URL generation, and parameter extraction with encoding type versioning.
113+
*/
114+
export class CodeShareManager {
115+
/** @type {string} */
116+
static CURRENT_ENCODING = 'gzip-b64';
117+
118+
/**
119+
* Available encoding types for future extensibility.
120+
* @type {Object<string, {compress: function, decompress: function}>}
121+
*/
122+
static ENCODERS = {
123+
'gzip-b64': {
124+
compress: CodeCompression.compress.bind(CodeCompression),
125+
decompress: CodeCompression.decompress.bind(CodeCompression)
126+
}
127+
};
128+
129+
/**
130+
* Generates a shareable URL with compressed code and encoding type.
131+
* @param {Object} code - Code object containing swift and dts properties
132+
* @param {string} code.swift - Swift code
133+
* @param {string} code.dts - TypeScript definition code
134+
* @returns {Promise<string>} Shareable URL
135+
*/
136+
static async generateShareUrl(code) {
137+
const codeData = JSON.stringify(code);
138+
const encoder = this.ENCODERS[this.CURRENT_ENCODING];
139+
140+
if (!encoder) {
141+
throw new Error(`Unsupported encoding type: ${this.CURRENT_ENCODING}`);
142+
}
143+
144+
const compressed = await encoder.compress(codeData);
145+
146+
const url = new URL(window.location.href);
147+
url.searchParams.set('code', compressed);
148+
url.searchParams.set('enc', this.CURRENT_ENCODING);
149+
150+
return url.toString();
151+
}
152+
153+
/**
154+
* Extracts code from URL parameters with encoding type detection.
155+
* @param {string} [url] - URL to extract from (defaults to current URL)
156+
* @returns {Promise<Object|null>} Code object or null if no code found
157+
*/
158+
static async extractCodeFromUrl(url) {
159+
const urlObj = new URL(url || window.location.href);
160+
const compressedCode = urlObj.searchParams.get('code');
161+
const encodingType = urlObj.searchParams.get('enc') || this.CURRENT_ENCODING;
162+
163+
if (!compressedCode) {
164+
return null;
165+
}
166+
167+
const encoder = this.ENCODERS[encodingType];
168+
if (!encoder) {
169+
console.error(`Unsupported encoding type: ${encodingType}`);
170+
throw new Error(`Unsupported encoding type: ${encodingType}. Supported types: ${Object.keys(this.ENCODERS).join(', ')}`);
171+
}
172+
173+
try {
174+
const decompressed = await encoder.decompress(compressedCode);
175+
return JSON.parse(decompressed);
176+
} catch (error) {
177+
console.error('Failed to extract code from URL:', error);
178+
throw new Error(`Failed to decode shared code (encoding: ${encodingType}): ${error.message}`);
179+
}
180+
}
181+
182+
/**
183+
* Checks if current URL contains shared code.
184+
* @returns {boolean} True if URL contains code parameter
185+
*/
186+
static hasSharedCode() {
187+
return new URL(window.location.href).searchParams.has('code');
188+
}
189+
}

0 commit comments

Comments
 (0)