Skip to content

Commit

Permalink
compress shared code snippets and ensure to have valid utf-8 handling
Browse files Browse the repository at this point in the history
lebalz committed Aug 3, 2024
1 parent dfbfacc commit 01dcf75
Showing 8 changed files with 112 additions and 87 deletions.
2 changes: 1 addition & 1 deletion src/theme/CodeEditor/Actions/DownloadCode.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useStore, useScript } from '@theme/CodeEditor/hooks';
import Button, { Color } from '@theme/CodeEditor/Button';
import Button from '@theme/CodeEditor/Button';
import { translate } from '@docusaurus/Translate';

const DownloadCode = (props: { title: string }) => {
2 changes: 1 addition & 1 deletion website/docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ const config: Config = {
label: 'Demo'
},
{
to: 'snippet',
to: 'snippets',
position: 'left',
label: 'Shareable Snippet'
},
1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
"@mdx-js/react": "3.0.1",
"clsx": "^2.1.1",
"docusaurus-live-brython": "link:../lib/",
"pako": "^2.1.0",
"prism-react-renderer": "^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
40 changes: 12 additions & 28 deletions website/src/components/SnippetComponents/Description/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,25 @@
import React from 'react';
import styles from './styles.module.css';
import { useLocation, useHistory } from '@docusaurus/router';
import useAutosizeTextArea from './useAutoSizeTextArea';

const Description = () => {
const location = useLocation();
const history = useHistory();
const [textAreaRef, setTextAreaRef] = React.useState<HTMLTextAreaElement>(null);
const [description, setDescription] = React.useState('');
useAutosizeTextArea(textAreaRef, '', 2);

React.useEffect(() => {
if (location.search) {
const params = new URLSearchParams(location.search);
if (params.has('description')) {
setDescription(params.get('description') || '');
}
}
}, []);
interface Props {
onChange: (title: string) => void;
description?: string;
}

React.useEffect(() => {
const params = new URLSearchParams(location.search);
if (description) {
params.set('description', description);
} else if (params.has('description')) {
params.delete('description');
}
history.replace({
search: params.toString()
});
}, [description]);
const Description = (props: Props) => {
const [textAreaRef, setTextAreaRef] = React.useState<HTMLTextAreaElement>(null);
const [description, setDescription] = React.useState(props.description || '');
useAutosizeTextArea(textAreaRef, props.description || '', 2);

return (
<textarea
value={description}
ref={setTextAreaRef}
onChange={(e) => setDescription(e.target.value)}
onChange={(e) => {
setDescription(e.target.value);
props.onChange(e.target.value);
}}
placeholder="Snippet Description"
className={styles.input}
/>
37 changes: 7 additions & 30 deletions website/src/components/SnippetComponents/Title/index.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,23 @@
import React from 'react';
import styles from './styles.module.css';
import { useLocation, useHistory } from '@docusaurus/router';
import clsx from 'clsx';

interface Props {
onChange?: (title: string) => void;
onChange: (title: string) => void;
title?: string;
}

const Title = (props: Props) => {
const location = useLocation();
const history = useHistory();
const [title, setTitle] = React.useState('');

React.useEffect(() => {
if (location.search) {
const params = new URLSearchParams(location.search);
if (params.has('title')) {
setTitle(params.get('title') || '');
}
}
}, []);

React.useEffect(() => {
const params = new URLSearchParams(location.search);
if (title) {
params.set('title', title);
} else if (params.has('title')) {
params.delete('title');
}
history.replace({
search: params.toString()
});
if (props.onChange) {
props.onChange(title);
}
}, [title]);
const [title, setTitle] = React.useState(props.title || '');

return (
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onChange={(e) => {
setTitle(e.target.value)
props.onChange(e.target.value)
}}
placeholder="Snippet Title"
className={clsx(styles.titleInput)}
/>
2 changes: 1 addition & 1 deletion website/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ function HomepageHeader() {
<Link className="button button--secondary button--lg" to={withBaseUrl('/demo')}>
Live Demo
</Link>
<Link className="button button--secondary button--lg" to={withBaseUrl('/snippet')}>
<Link className="button button--secondary button--lg" to={withBaseUrl('/snippets')}>
Share Code
</Link>
</div>
110 changes: 84 additions & 26 deletions website/src/pages/snippet.tsx → website/src/pages/snippets.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,79 @@
import clsx from 'clsx';
import Layout from '@theme/Layout';
import Heading from '@theme/Heading';
import useBaseUrl from '@docusaurus/useBaseUrl';
// @ts-ignore
import ContextEditor from '@theme/CodeEditor/ContextEditor';
import styles from './styles.module.css';
import { useLocation, useHistory } from '@docusaurus/router';
import React from 'react';
import Title from '../components/SnippetComponents/Title';
import Description from '../components/SnippetComponents/Description';
import pako from 'pako';

interface CodeSnippet {
title: string;
description: string;
code: string;
}
const DEFAULT_SNIPPET: CodeSnippet = Object.freeze({
title: '',
description: '',
code: ''
});
const encodeToBase64 = (str: string) => {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const compressed = pako.gzip(data);
return btoa(String.fromCharCode(...new Uint8Array(compressed)));
};

const decodeFromBase64 = (base64: string) => {
const binaryString = atob(base64);
const binaryLen = binaryString.length;
const bytes = new Uint8Array(binaryLen);
for (let i = 0; i < binaryLen; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const decompressed = pako.ungzip(bytes);
const decoder = new TextDecoder();
return decoder.decode(decompressed);
};

const decodeSnippet = (raw: string): CodeSnippet => {
try {
const snippet = JSON.parse(decodeFromBase64(decodeURIComponent(raw)));
return {...DEFAULT_SNIPPET, ...snippet};
} catch (e) {
console.warn('Failed to decode snippet', e);
return {...DEFAULT_SNIPPET};
}
};

const encodeSnippet = (snippet: CodeSnippet): string => {
return encodeURIComponent(encodeToBase64(JSON.stringify(snippet)));
};

export default function Snippet(): JSX.Element {
const location = useLocation();
const history = useHistory();
const [initialized, setInitialized] = React.useState(false);
const [code, setCode] = React.useState<string>('');
const [edited, setEdited] = React.useState('');
const [code, setCode] = React.useState('');
const [init, setInit] = React.useState<CodeSnippet>({...DEFAULT_SNIPPET});
const [title, setTitle] = React.useState('');
const [description, setDescription] = React.useState('');

const [copyied, setCopyied] = React.useState(false);

React.useEffect(() => {
if (location.search) {
const params = new URLSearchParams(location.search);
if (params.has('code')) {
const snippet = params.get('code') || '';
console.log(snippet);
setCode(snippet);
setEdited(snippet);
if (params.has('snippet')) {
const raw = params.get('snippet');
const snippet = decodeSnippet(raw);
setCode(snippet.code);
setTitle(snippet.title);
setDescription(snippet.description);
setInit(snippet);
}
}
setInitialized(true);
@@ -36,16 +83,25 @@ export default function Snippet(): JSX.Element {
if (!initialized) {
return;
}
const params = new URLSearchParams(location.search);
if (edited) {
params.set('code', edited);
} else if (params.has('code')) {
params.delete('code');
const data: Partial<CodeSnippet> = {};
if (code.length > 0) {
data.code = code;
}
if (title.length > 0) {
data.title = title;
}
if (description.length > 0) {
data.description = description;
}
if (Object.keys(data).length === 0) {
return;
}
const enc = encodeSnippet(data as CodeSnippet);
console.log(decodeSnippet(enc), enc);
history.replace({
search: params.toString()
search: enc ? `?snippet=${enc}` : ''
});
}, [initialized, edited]);
}, [initialized, code, title, description]);

React.useEffect(() => {
const timeoutId = setTimeout(() => {
@@ -91,18 +147,20 @@ export default function Snippet(): JSX.Element {
<p style={{ marginBottom: 0 }}>
Share your Python code snippets by editing the code below and then sharing the link.
</p>
<div>
<Title onChange={(title) => setTitle(title)} />
<Description />
</div>
{initialized && (
<ContextEditor
className={clsx('language-py')}
title={title || 'snippet.py'}
onChange={(code: string) => setEdited(code)}
>
{code || "print('Hello Python Snippet')"}
</ContextEditor>
<>
<div>
<Title onChange={setTitle} title={init.title} />
<Description onChange={setDescription} description={init.description} />
</div>
<ContextEditor
className={clsx('language-py')}
title={title || 'snippet.py'}
onChange={(code: string) => setCode(code)}
>
{init.code || "print('Hello Python Snippet')"}
</ContextEditor>
</>
)}
</div>
</main>
5 changes: 5 additions & 0 deletions website/yarn.lock
Original file line number Diff line number Diff line change
@@ -6261,6 +6261,11 @@ package-json@^8.1.0:
registry-url "^6.0.0"
semver "^7.3.7"

pako@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==

param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"

0 comments on commit 01dcf75

Please sign in to comment.