Skip to content

Commit 20ca2bc

Browse files
committed
docs - add share option
1 parent e5caba3 commit 20ca2bc

File tree

4 files changed

+168
-23
lines changed

4 files changed

+168
-23
lines changed

docs/src/components/TransformerTester.tsx

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import useJSONString from "../components/hooks/useJSONString";
55
import {setSuggestions} from "@site/src/components/monaco/suggestionsProvider";
66
import {jsonpathJoin} from "@nlighten/json-transform-core";
77
import {JsonTransformer} from "@nlighten/json-transform";
8+
import JSONGzip from "@site/src/utils/JSONGzip";
9+
import {useLocation, useHistory} from "@docusaurus/router";
810

911
const DEFAULT_INPUT_VALUE = `{
1012
"first_name": "John",
@@ -44,6 +46,9 @@ const transformAsync = async (input: any, definition: any) => {
4446
const narrowScreen = window.innerWidth < 996;
4547
const initialHeight = narrowScreen ? 280 : 450;
4648

49+
export const SHARE_QS = "shared";
50+
export const GZIP_MARKER = "gzip,";
51+
4752
const TransformerTester = () => {
4853
const [inputString, setInputString, parsedInput, inputError] = useJSONString(DEFAULT_INPUT_VALUE);
4954
const [transformerString, setTransformerString, parsedTransformer, transformerError] =
@@ -52,6 +57,9 @@ const TransformerTester = () => {
5257
const [outputError, setOutputError] = useState<string | null>(null);
5358
const [loading, setLoading] = useState(false);
5459

60+
const location = useLocation()
61+
const history = useHistory()
62+
5563
useEffect(() => {
5664
if (parsedInput) {
5765
const generatedSchema = JSONSchemaUtils.generate(parsedInput);
@@ -81,28 +89,95 @@ const TransformerTester = () => {
8189
.finally(() => setLoading(false));
8290
}
8391

84-
return <div className="tester_grid">
85-
<h3 className="desktop-title">Input</h3>
86-
<h3 className="desktop-title">Transformer</h3>
87-
<h3 className="desktop-title">Output</h3>
88-
<MonacoEditor language="json" value={inputString} onChange={(value) => setInputString(value)} height={initialHeight}/>
89-
<MonacoEditor language="json" model="transformer.json"
90-
value={transformerString}
91-
validateVariables
92-
onChange={(value) => setTransformerString(value)}
93-
height={initialHeight}/>
94-
<MonacoEditor language="json" readOnly value={outputJSONString} height={initialHeight}/>
95-
<div style={{color: 'red'}}>{inputError?.message}</div>
96-
<div className="button-container">
97-
<div style={{color: 'red'}}>{transformerError?.message}</div>
98-
<button type="button" className="button button--primary transform-button shadow--lw"
99-
disabled={loading || !!(inputError || transformerError)}
100-
onClick={handleTransform}>Transform
101-
</button>
102-
<div data-loader className={loading ? undefined:"hide" }></div>
92+
useEffect(() => {
93+
const searchIndex = location.hash?.indexOf("?");
94+
if (searchIndex === -1 || !location.hash.includes(SHARE_QS + "=", searchIndex)) return;
95+
const sp = new URLSearchParams(location.hash.substring(searchIndex + 1));
96+
if (!sp.has(SHARE_QS)) return;
97+
const sharedValue = sp.get(SHARE_QS);
98+
if (!sharedValue) return;
99+
try {
100+
(sharedValue.startsWith(GZIP_MARKER)
101+
? JSONGzip.decompress(sharedValue.substring(GZIP_MARKER.length), "base64url")
102+
: Promise.resolve(JSON.parse(sharedValue))
103+
)
104+
.then(defaults => {
105+
setInputString(JSON.stringify(defaults.i, null, 2));
106+
setTransformerString(JSON.stringify(defaults.d, null, 2));
107+
history.replace("#");
108+
})
109+
.catch(e => {
110+
console.error(e);
111+
history.replace("#");
112+
});
113+
} catch (e: any) {
114+
console.error(e);
115+
history.replace("#");
116+
}
117+
}, [location.hash, history.replace]);
118+
119+
const handleShare = async () => {
120+
const input = JSON.stringify({i: parsedInput, d: parsedTransformer});
121+
const compressed = await JSONGzip.compress(input, "base64url", "string");
122+
const encodedInput = encodeURIComponent(input);
123+
const sharedValue = encodedInput.length > compressed.length + GZIP_MARKER.length
124+
? GZIP_MARKER + compressed // compressed is better
125+
: encodedInput;
126+
127+
prompt("Copy this value and share it", window.location.origin + window.location.pathname + `#?${SHARE_QS}=` + sharedValue);
128+
};
129+
130+
return (
131+
<div className="tester_grid">
132+
<h3 className="hide-on-mobile">Input</h3>
133+
<h3 className="hide-on-mobile">Transformer</h3>
134+
<h3 style={{display: "flex", flexDirection: "row-reverse", justifyContent: "space-between"}}>
135+
<button type="button" className="button button--primary button--sm share-button shadow--lw"
136+
disabled={!parsedTransformer}
137+
onClick={handleShare}>
138+
<span style={{display: 'flex', alignItems: "center", gap: "4px"}}>
139+
<svg className="MuiSvgIcon-root" focusable="false" aria-hidden="true"
140+
viewBox="0 0 24 24" width={16} height={16} fill="currentcolor">
141+
<path
142+
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92"></path>
143+
</svg>
144+
Share Input and Transformer
145+
</span>
146+
</button>
147+
<span className="hide-on-mobile">Output</span>
148+
</h3>
149+
<MonacoEditor language="json" value={inputString} onChange={(value) => setInputString(value)}
150+
height={initialHeight}/>
151+
<MonacoEditor language="json" model="transformer.json"
152+
value={transformerString}
153+
validateVariables
154+
onChange={(value) => setTransformerString(value)}
155+
height={initialHeight}/>
156+
<MonacoEditor language="json" readOnly value={outputJSONString} height={initialHeight}/>
157+
<div style={{color: 'red', fontSize: "0.7em"}}>{inputError?.message}</div>
158+
<div className="button-container">
159+
<div style={{color: 'red', fontSize: "0.7em"}}>{transformerError?.message}</div>
160+
<button type="button" className="button button--primary transform-button shadow--lw"
161+
disabled={loading || !!(inputError || transformerError)}
162+
onClick={handleTransform}>Transform
163+
</button>
164+
<div data-loader className={loading ? undefined : "hide"}></div>
165+
</div>
166+
<div style={
167+
outputError
168+
? {color: 'red', fontSize: "0.7em"}
169+
: outputJSONString
170+
? {fontSize: "0.7em"}
171+
: undefined
172+
}>
173+
{outputError ??
174+
(outputJSONString
175+
? "* You can open the browser console to see debug information about the transformation"
176+
: undefined)
177+
}
178+
</div>
103179
</div>
104-
<div style={outputError ? {color: 'red'} : outputJSONString ? { fontSize: "0.7em"} : undefined}>{outputError ?? (outputJSONString ? "* You can open the browser console to see debug information about the transformation" : undefined)}</div>
105-
</div>
180+
);
106181
}
107182

108183
export default TransformerTester;

docs/src/css/custom.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
.tester_grid {
5555
grid-template-columns: auto;
5656
}
57-
.desktop-title, p, h3 {
57+
.hide-on-mobile {
5858
display: none;
5959
}
6060
}

docs/src/pages/playground.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const playground = () => {
1717
<Layout>
1818
<div className="container" style={{
1919
// @ts-ignore
20-
"--ifm-container-width-xl": "1600px"
20+
"--ifm-container-width-xl": "1600px",
21+
paddingBottom: "10px"
2122
}}>
2223
<br/>
2324
<h1>Playground</h1>

docs/src/utils/JSONGzip.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
const Base64Url = {
2+
from(b64: string) {
3+
return b64.replace(/=+/g, "").replace(/\+/g, "-").replace(/\//g, "_");
4+
},
5+
toBase64(b64Url: string) {
6+
return b64Url.replace(/-/g, "+").replace(/_/g, "/");
7+
},
8+
};
9+
10+
const blobToBase64 = (blob: Blob): Promise<string> => {
11+
const reader = new FileReader();
12+
reader.readAsDataURL(blob);
13+
return new Promise(resolve => {
14+
reader.onloadend = () => {
15+
const dataUrl = reader.result as string;
16+
resolve(dataUrl.substring(dataUrl.indexOf(";") + 1));
17+
};
18+
});
19+
};
20+
21+
const base64ToUint8Array = (base64: string) => {
22+
base64 = base64.replace(/[\s\t\n]/g, "");
23+
if (base64.length % 4 !== 0) {
24+
base64 = base64 + "=".repeat(4 - (base64.length % 4));
25+
}
26+
const bytesString = atob(base64);
27+
const length = bytesString.length;
28+
const bytes = new Uint8Array(length);
29+
for (let i = 0; i < length; i++) {
30+
bytes[i] = bytesString.charCodeAt(i);
31+
}
32+
return bytes;
33+
};
34+
35+
class JSONGzip {
36+
public static async compress(
37+
value: string,
38+
format: "base64" | "base64url" = "base64",
39+
valueType: "string" | "json" = "json",
40+
) {
41+
const stream = new Blob([valueType === "string" ? value : JSON.stringify(value)], {
42+
type: "application/json",
43+
}).stream();
44+
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
45+
const response = new Response(compressedStream);
46+
const blob = await response.blob();
47+
const base64 = await blobToBase64(blob);
48+
// const buffer = await response.blob().then(blob => blob.arrayBuffer());
49+
// const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
50+
if (format === "base64url") {
51+
return Base64Url.from(base64);
52+
}
53+
return base64;
54+
}
55+
56+
public static async decompress(compressed: string, format: "base64" | "base64url" = "base64") {
57+
const base64Input = format === "base64url" ? Base64Url.toBase64(compressed) : compressed;
58+
const buffer = base64ToUint8Array(base64Input);
59+
const stream = new Blob([buffer], { type: "text/plain" }).stream();
60+
const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip"));
61+
const response = new Response(decompressedStream);
62+
return response
63+
.blob()
64+
.then(blob => blob.text())
65+
.then(text => JSON.parse(text));
66+
}
67+
}
68+
69+
export default JSONGzip;

0 commit comments

Comments
 (0)