+ This URL is {urlMetadata?.length || 0} characters long. Some servers may reject URLs longer than {urlMetadata?.maxLength || 4000} characters. Consider using the "Save File" option for complex graphs.
+
+
+ )}
{/* URL input and copy button */}
diff --git a/src/utils/urlSharing.js b/src/utils/urlSharing.js
index 2195d09..9fc4c4b 100644
--- a/src/utils/urlSharing.js
+++ b/src/utils/urlSharing.js
@@ -2,40 +2,52 @@
* URL sharing utilities for PathView
* Handles encoding and decoding graph data in URLs
*/
+import { compressToBase64, decompressFromBase64 } from 'lz-string';
+
+// Maximum safe URL length for most servers (conservative estimate)
+const MAX_SAFE_URL_LENGTH = 4000;
/**
- * Encode graph data to a base64 URL parameter
+ * Encode graph data to a compressed base64 URL parameter
* @param {Object} graphData - The complete graph data object
- * @returns {string} - Base64 encoded string
+ * @returns {string} - Compressed base64 encoded string
*/
export function encodeGraphData(graphData) {
- try {
- const jsonString = JSON.stringify(graphData);
- // Use btoa for base64 encoding, but handle Unicode strings properly
- const utf8Bytes = new TextEncoder().encode(jsonString);
- const binaryString = Array.from(utf8Bytes, byte => String.fromCharCode(byte)).join('');
- return btoa(binaryString);
- } catch (error) {
- console.error('Error encoding graph data:', error);
- return null;
- }
-}
-
-/**
- * Decode graph data from a base64 URL parameter
- * @param {string} encodedData - Base64 encoded graph data
+ try {
+ const jsonString = JSON.stringify(graphData);
+ // Use lz-string for much better compression than manual whitespace removal
+ return compressToBase64(jsonString);
+ } catch (error) {
+ console.error('Error encoding graph data:', error);
+ return null;
+ }
+}/**
+ * Decode graph data from a compressed base64 URL parameter
+ * @param {string} encodedData - Compressed base64 encoded graph data
* @returns {Object|null} - Decoded graph data object or null if error
*/
export function decodeGraphData(encodedData) {
try {
- // Decode base64 and handle Unicode properly
- const binaryString = atob(encodedData);
- const bytes = new Uint8Array(binaryString.length);
- for (let i = 0; i < binaryString.length; i++) {
- bytes[i] = binaryString.charCodeAt(i);
+ // First try lz-string decompression (new format)
+ const jsonString = decompressFromBase64(encodedData);
+ if (jsonString) {
+ return JSON.parse(jsonString);
+ }
+
+ // Fallback for old format (manual base64 encoding)
+ try {
+ const binaryString = atob(encodedData);
+ const bytes = new Uint8Array(binaryString.length);
+ for (let i = 0; i < binaryString.length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ const oldJsonString = new TextDecoder().decode(bytes);
+ return JSON.parse(oldJsonString);
+ } catch (oldFormatError) {
+ console.warn('Could not decode with old format either:', oldFormatError);
}
- const jsonString = new TextDecoder().decode(bytes);
- return JSON.parse(jsonString);
+
+ return null;
} catch (error) {
console.error('Error decoding graph data:', error);
return null;
@@ -45,27 +57,32 @@ export function decodeGraphData(encodedData) {
/**
* Generate a shareable URL with the current graph data
* @param {Object} graphData - The complete graph data object
- * @returns {string} - Complete shareable URL
+ * @returns {Object} - Object with url and metadata about the URL
*/
export function generateShareableURL(graphData) {
- try {
- const encodedData = encodeGraphData(graphData);
- if (!encodedData) {
- throw new Error('Failed to encode graph data');
- }
-
- const baseURL = window.location.origin + window.location.pathname;
- const url = new URL(baseURL);
- url.searchParams.set('graph', encodedData);
-
- return url.toString();
- } catch (error) {
- console.error('Error generating shareable URL:', error);
- return null;
+ try {
+ const encodedData = encodeGraphData(graphData);
+ if (!encodedData) {
+ throw new Error('Failed to encode graph data');
}
-}
-
-/**
+
+ const baseURL = window.location.origin + window.location.pathname;
+ const url = new URL(baseURL);
+ url.searchParams.set('graph', encodedData);
+
+ const finalURL = url.toString();
+
+ return {
+ url: finalURL,
+ length: finalURL.length,
+ isSafe: finalURL.length <= MAX_SAFE_URL_LENGTH,
+ maxLength: MAX_SAFE_URL_LENGTH
+ };
+ } catch (error) {
+ console.error('Error generating shareable URL:', error);
+ return null;
+ }
+}/**
* Extract graph data from current URL parameters
* @returns {Object|null} - Graph data object or null if not found/error
*/
@@ -91,21 +108,21 @@ export function getGraphDataFromURL() {
* @param {boolean} replaceState - Whether to replace current history state (default: false)
*/
export function updateURLWithGraphData(graphData, replaceState = false) {
- try {
- const shareableURL = generateShareableURL(graphData);
- if (shareableURL) {
- if (replaceState) {
- window.history.replaceState({}, '', shareableURL);
- } else {
- window.history.pushState({}, '', shareableURL);
- }
- }
- } catch (error) {
- console.error('Error updating URL with graph data:', error);
+ try {
+ const urlResult = generateShareableURL(graphData);
+ if (urlResult && urlResult.isSafe) {
+ if (replaceState) {
+ window.history.replaceState({}, '', urlResult.url);
+ } else {
+ window.history.pushState({}, '', urlResult.url);
+ }
+ } else if (urlResult) {
+ console.warn(`URL too long (${urlResult.length} chars), not updating browser URL`);
}
-}
-
-/**
+ } catch (error) {
+ console.error('Error updating URL with graph data:', error);
+ }
+}/**
* Clear graph data from URL without page reload
*/
export function clearGraphDataFromURL() {
@@ -116,3 +133,54 @@ export function clearGraphDataFromURL() {
console.error('Error clearing graph data from URL:', error);
}
}
+
+/**
+ * Copy shareable URL to clipboard
+ * @param {Object} graphData - The complete graph data object
+ * @returns {Promise