Convert to typescript

This commit is contained in:
Luck
2022-11-06 22:46:13 +00:00
parent e995261d87
commit 042bace2c6
18 changed files with 593 additions and 321 deletions

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.idea

View File

@@ -3,26 +3,40 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "4.4.6",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"content-type-parser": "^1.0.2",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"history": "^5.0.0", "history": "^5.0.0",
"local-storage": "^2.0.0", "local-storage": "^2.0.0",
"pako": "^2.0.3", "pako": "^2.0.3",
"react": "^18.1.0", "react": "^18.2.0",
"react-device-detect": "^2.1.2", "react-device-detect": "^2.1.2",
"react-dom": "^18.1.0", "react-dom": "^18.2.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"styled-components": "^5.2.1" "styled-components": "^5.2.1",
"typescript": "^4.8.4",
"whatwg-mimetype": "^3.0.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.2.0",
"@types/jest": "^29.2.2",
"@types/node": "^18.11.9",
"@types/pako": "^2.0.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"@types/styled-components": "^5.1.26",
"@types/whatwg-mimetype": "^3.0.0",
"monaco-editor": "^0.34.1",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.1.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"format": "prettier --write '**/*.ts' '**/*.tsx' '**/*.css'"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -1,62 +1,27 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import Editor from './components/Editor'; import Editor from './components/Editor';
import parseContentType from 'content-type-parser'; import { loadFromBytebin } from './util/storage';
import { languageIds } from './util/highlighting';
import { bytebinUrl } from './util/constants';
function getPasteIdFromUrl() {
const path = window.location.pathname;
if (path && /^\/[a-zA-Z0-9]+$/.test(path)) {
return path.substring(1);
} else {
return undefined;
}
}
async function loadFromBytebin(id) {
try {
const resp = await fetch(bytebinUrl + id);
if (resp.ok) {
const content = await resp.text();
const type = parseLanguageFromContentType(
resp.headers.get('content-type')
);
document.title = 'pastes | ' + id;
return { ok: true, content, type };
} else {
return { ok: false };
}
} catch (e) {
return { ok: false };
}
}
function parseLanguageFromContentType(contentType) {
const { type, subtype: subType } = parseContentType(contentType);
if (type === 'application' && subType === 'json') {
return 'json';
}
if (type === 'text' && languageIds.includes(subType.toLowerCase())) {
return subType.toLowerCase();
}
}
const INITIAL = Symbol(); const INITIAL = Symbol();
const LOADING = Symbol(); const LOADING = Symbol();
const LOADED = Symbol(); const LOADED = Symbol();
export default function App() { type LoadingState = typeof INITIAL | typeof LOADING | typeof LOADED;
const [pasteId] = useState(getPasteIdFromUrl);
const [state, setState] = useState(INITIAL);
const [forcedContent, setForcedContent] = useState('');
const [actualContent, setActualContent] = useState('');
const [contentType, setContentType] = useState();
const setContent = useCallback((content) => { export default function App() {
const [pasteId] = useState<string | undefined>(getPasteIdFromUrl);
const [state, setState] = useState<LoadingState>(INITIAL);
const [forcedContent, setForcedContent] = useState<string>('');
const [actualContent, setActualContent] = useState<string>('');
const [contentType, setContentType] = useState<string>();
const setContent = useCallback(
(content: string) => {
setActualContent(content); setActualContent(content);
setForcedContent(content); setForcedContent(content);
}, [setActualContent, setForcedContent]); },
[setActualContent, setForcedContent]
);
useEffect(() => { useEffect(() => {
if (pasteId && state === INITIAL) { if (pasteId && state === INITIAL) {
@@ -88,7 +53,7 @@ export default function App() {
); );
} }
function get404Message(pasteId) { function get404Message(pasteId: string) {
return ` return `
@@ -101,3 +66,12 @@ function get404Message(pasteId) {
maybe the paste expired? maybe the paste expired?
`; `;
} }
function getPasteIdFromUrl() {
const path = window.location.pathname;
if (path && /^\/[a-zA-Z0-9]+$/.test(path)) {
return path.substring(1);
} else {
return undefined;
}
}

23
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,23 @@
import styled from 'styled-components';
const Button = styled.div`
cursor: pointer;
height: 100%;
display: flex;
align-items: center;
padding: 0 0.25em;
color: inherit;
text-decoration: none;
:hover {
background: ${props => props.theme.highlight};
}
@media (max-width: 640px) {
span {
display: none;
}
}
`;
export default Button;

View File

@@ -1,11 +1,21 @@
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { ThemeProvider, createGlobalStyle } from 'styled-components';
import { isMobile } from 'react-device-detect'; import { isMobile } from 'react-device-detect';
import ls from 'local-storage'; import { ThemeProvider } from 'styled-components';
import usePreference from '../hooks/usePreference';
import themes, { Themes } from '../style/themes';
import EditorControls from './EditorControls'; import EditorControls from './EditorControls';
import EditorGlobalStyle from './EditorGlobalStyle';
import EditorTextArea from './EditorTextArea'; import EditorTextArea from './EditorTextArea';
import themes from '../style/themes';
export interface EditorProps {
forcedContent: string;
setForcedContent: (value: string) => void;
actualContent: string;
setActualContent: (value: string) => void;
contentType?: string;
pasteId?: string;
}
export default function Editor({ export default function Editor({
forcedContent, forcedContent,
@@ -14,16 +24,16 @@ export default function Editor({
setActualContent, setActualContent,
contentType, contentType,
pasteId, pasteId,
}) { }: EditorProps) {
const [language, setLanguage] = useState('plain'); const [language, setLanguage] = useState<string>('plain');
const [readOnly, setReadOnly] = useState(isMobile && pasteId); const [readOnly, setReadOnly] = useState<boolean>(isMobile && !!pasteId);
const [theme, setTheme] = usePreference( const [theme, setTheme] = usePreference<keyof Themes>(
'theme', 'theme',
'dark', 'dark',
pref => !!themes[pref] pref => !!themes[pref]
); );
const [fontSize, setFontSize, fontSizeCheck] = usePreference( const [fontSize, setFontSize, fontSizeCheck] = usePreference<number>(
'fontsize', 'fontsize',
16, 16,
pref => pref >= 10 && pref <= 22 pref => pref >= 10 && pref <= 22
@@ -35,7 +45,7 @@ export default function Editor({
} }
}, [contentType]); }, [contentType]);
function zoom(delta) { function zoom(delta: number) {
const newFontSize = fontSize + delta; const newFontSize = fontSize + delta;
if (fontSizeCheck(newFontSize)) { if (fontSizeCheck(newFontSize)) {
setFontSize(newFontSize); setFontSize(newFontSize);
@@ -65,39 +75,8 @@ export default function Editor({
language={language} language={language}
fontSize={fontSize} fontSize={fontSize}
readOnly={readOnly} readOnly={readOnly}
setReadOnly={setReadOnly}
/> />
</ThemeProvider> </ThemeProvider>
</> </>
); );
} }
const EditorGlobalStyle = createGlobalStyle`
html, body {
color-scheme: ${props => props.theme.lightOrDark};
scrollbar-color: ${props => props.theme.lightOrDark};
background-color: ${props => props.theme.editor.background};
}
`;
// hook used to load "preference" settings from local storage, or fall back to a default value.
function usePreference(id, defaultValue, valid) {
const [value, setValue] = useState(() => {
const pref = ls.get(id);
if (pref && valid(pref)) {
return pref;
} else {
return defaultValue;
}
});
useEffect(() => {
if (value === defaultValue) {
ls.remove(id);
} else {
ls.set(id, value);
}
}, [value, id, defaultValue]);
return [value, setValue, valid];
}

View File

@@ -1,13 +1,25 @@
import { useState, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import { gzip } from 'pako';
import history from 'history/browser';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import history from 'history/browser';
import { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { MenuButton, Button } from './Menu'; import themes, { Themes } from '../style/themes';
import { languages } from '../util/highlighting'; import { languages } from '../util/highlighting';
import themes from '../style/themes'; import { saveToBytebin } from '../util/storage';
import { postUrl } from '../util/constants'; import Button from './Button';
import MenuButton from './MenuButton';
export interface EditorControlsProps {
actualContent: string;
setForcedContent: (value: string) => void;
language: string;
setLanguage: (value: string) => void;
readOnly: boolean;
setReadOnly: (value: boolean) => void;
theme: keyof Themes;
setTheme: (value: keyof Themes) => void;
zoom: (delta: number) => void;
}
export default function EditorControls({ export default function EditorControls({
actualContent, actualContent,
@@ -19,9 +31,9 @@ export default function EditorControls({
theme, theme,
setTheme, setTheme,
zoom, zoom,
}) { }: EditorControlsProps) {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState<boolean>(false);
const [recentlySaved, setRecentlySaved] = useState(false); const [recentlySaved, setRecentlySaved] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setRecentlySaved(false); setRecentlySaved(false);
@@ -46,7 +58,7 @@ export default function EditorControls({
}, [actualContent, language, recentlySaved]); }, [actualContent, language, recentlySaved]);
useEffect(() => { useEffect(() => {
const listener = e => { const listener = (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
if (e.key === 's' || e.key === 'S') { if (e.key === 's' || e.key === 'S') {
e.preventDefault(); e.preventDefault();
@@ -100,7 +112,7 @@ export default function EditorControls({
label="theme" label="theme"
value={theme} value={theme}
setValue={setTheme} setValue={setTheme}
ids={Object.keys(themes)} ids={Object.keys(themes) as (keyof Themes)[]}
/> />
<Button <Button
className="optional" className="optional"
@@ -139,36 +151,3 @@ const Section = styled.div`
} }
} }
`; `;
function langaugeToContentType(language) {
if (language === 'json') {
return 'application/json';
} else {
return 'text/' + language;
}
}
async function saveToBytebin(code, language) {
try {
const compressed = gzip(code);
const contentType = langaugeToContentType(language);
const resp = await fetch(postUrl, {
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Encoding': 'gzip',
'Accept': 'application/json',
},
body: compressed,
});
if (resp.ok) {
const json = await resp.json();
return json.key;
}
} catch (e) {
console.log(e);
}
return null;
}

View File

@@ -0,0 +1,12 @@
import { createGlobalStyle, ThemeProps } from 'styled-components';
import { Theme } from '../style/themes';
const EditorGlobalStyle = createGlobalStyle<ThemeProps<Theme>>`
html, body {
color-scheme: ${props => props.theme.lightOrDark};
scrollbar-color: ${props => props.theme.lightOrDark};
background-color: ${props => props.theme.editor.background};
}
`;
export default EditorGlobalStyle;

View File

@@ -1,8 +1,25 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import Editor, {
BeforeMount,
Monaco,
OnChange,
OnMount,
} from '@monaco-editor/react';
import history from 'history/browser'; import history from 'history/browser';
import Editor from '@monaco-editor/react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import themes, { makeMonacoTheme } from '../style/themes'; import themes, { makeMonacoTheme, Theme } from '../style/themes';
import type { editor } from 'monaco-editor';
export interface EditorTextAreaProps {
forcedContent: string;
actualContent: string;
setActualContent: (value: string) => void;
theme: Theme;
language: string;
fontSize: number;
readOnly: boolean;
}
export default function EditorTextArea({ export default function EditorTextArea({
forcedContent, forcedContent,
@@ -12,11 +29,12 @@ export default function EditorTextArea({
language, language,
fontSize, fontSize,
readOnly, readOnly,
}) { }: EditorTextAreaProps) {
const [editor, setEditor] = useState(); const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const [monaco, setMonaco] = useState(); const [monaco, setMonaco] = useState<Monaco>();
const [selected, toggleSelected] = useSelectedLine(); const [selected, toggleSelected] = useSelectedLine();
const editorAreaRef = useRef(); const editorAreaRef = useRef<HTMLDivElement>(null);
useLineNumberMagic( useLineNumberMagic(
editorAreaRef, editorAreaRef,
selected, selected,
@@ -25,9 +43,10 @@ export default function EditorTextArea({
editor, editor,
monaco monaco
); );
usePlaceholderText(actualContent, editor, monaco); usePlaceholderText(actualContent, editor, monaco);
function beforeMount(monaco) { const beforeMount: BeforeMount = monaco => {
for (const [id, theme] of Object.entries(themes)) { for (const [id, theme] of Object.entries(themes)) {
monaco.editor.defineTheme(id, makeMonacoTheme(theme)); monaco.editor.defineTheme(id, makeMonacoTheme(theme));
} }
@@ -36,32 +55,35 @@ export default function EditorTextArea({
noSemanticValidation: true, noSemanticValidation: true,
noSyntaxValidation: true, noSyntaxValidation: true,
}); });
} };
function onMount(editor, monaco) { const onMount: OnMount = (editor, monaco) => {
editor.addAction({ editor.addAction({
id: 'search', id: 'search',
label: 'Search with Google', label: 'Search with Google',
contextMenuGroupId: '9_cutcopypaste', contextMenuGroupId: '9_cutcopypaste',
contextMenuOrder: 5, contextMenuOrder: 5,
run: (editor) => { run: editor => {
const selection = editor.getSelection() const selection = editor.getSelection();
if (!selection.isEmpty()) { if (selection && !selection.isEmpty()) {
const query = editor.getModel().getValueInRange(selection); const model = editor.getModel();
if (model) {
const query = model.getValueInRange(selection);
window.open('https://www.google.com/search?q=' + query, '_blank'); window.open('https://www.google.com/search?q=' + query, '_blank');
} }
} }
}) },
});
setEditor(editor); setEditor(editor);
setMonaco(monaco); setMonaco(monaco);
editor.focus(); editor.focus();
} };
const onChange = useCallback( const onChange: OnChange = useCallback(
value => { value => {
setActualContent(value); setActualContent(value as string);
}, },
[setActualContent] [setActualContent]
); );
@@ -72,8 +94,13 @@ export default function EditorTextArea({
return; return;
} }
editor.getModel().detectIndentation(true, 2); const model = editor.getModel();
}, [editor, forcedContent]) if (!model) {
return;
}
model.detectIndentation(true, 2);
}, [editor, forcedContent]);
return ( return (
<EditorArea ref={editorAreaRef}> <EditorArea ref={editorAreaRef}>
@@ -84,9 +111,9 @@ export default function EditorTextArea({
fontFamily: 'JetBrains Mono', fontFamily: 'JetBrains Mono',
fontSize: fontSize, fontSize: fontSize,
fontLigatures: true, fontLigatures: true,
wordWrap: true, wordWrap: 'on',
renderLineHighlight: 'none', renderLineHighlight: 'none',
renderValidationDecorations: false, renderValidationDecorations: 'off',
readOnly, readOnly,
domReadOnly: readOnly, domReadOnly: readOnly,
}} }}
@@ -124,13 +151,17 @@ const EditorArea = styled.main`
} }
`; `;
function usePlaceholderText(actualContent, editor, monaco) { function usePlaceholderText(
actualContent: string,
editor: editor.IStandaloneCodeEditor | undefined,
monaco: Monaco | undefined
) {
useEffect(() => { useEffect(() => {
if (!editor || !monaco || actualContent) { if (!editor || !monaco || actualContent) {
return; return;
} }
const decoration = { const decoration = {
range: new monaco.Range(1, 1, 1, 1), range: new monaco.Range(1, 0, 1, 1),
options: { options: {
after: { after: {
content: '\u200B', content: '\u200B',
@@ -146,13 +177,63 @@ function usePlaceholderText(actualContent, editor, monaco) {
}, [editor, monaco, actualContent]); }, [editor, monaco, actualContent]);
} }
type SelectedLine = [number, number];
type ToggleSelectedFunction = (lineNo: number, e: MouseEvent) => void;
function useSelectedLine(): [SelectedLine, ToggleSelectedFunction] {
// extract highlighted lines from window hash
const [selected, setSelected] = useState<[number, number]>(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? start : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
let hash = '';
if (selected[0] !== -1) {
if (selected[1] !== selected[0]) {
const start = Math.min(...selected);
const end = Math.max(...selected);
hash = `#L${start}-${end}`;
} else {
hash = `#L${selected[0]}`;
}
}
history.replace({ hash });
}, [selected]);
// toggle the highlighting for a given line
const toggleSelected = useCallback(
(lineNo: number, e: MouseEvent) => {
const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, lineNo]);
} else {
setSelected([selected[0], lineNo]);
}
},
[selected, setSelected]
);
return [selected, toggleSelected];
}
function useLineNumberMagic( function useLineNumberMagic(
editorAreaRef, editorAreaRef: React.RefObject<HTMLDivElement>,
selected, selected: SelectedLine,
toggleSelected, toggleSelected: ToggleSelectedFunction,
forcedContent, forcedContent: string,
editor, editor: editor.IStandaloneCodeEditor | undefined,
monaco monaco: Monaco | undefined
) { ) {
// add an event listener for clicking on line numbers // add an event listener for clicking on line numbers
useEffect(() => { useEffect(() => {
@@ -161,9 +242,13 @@ function useLineNumberMagic(
return; return;
} }
const handler = click => { const handler = (click: MouseEvent) => {
const target = click?.target; const target = click?.target as HTMLElement;
if (target && target.classList.contains('line-numbers')) { if (
target &&
target.classList.contains('line-numbers') &&
target.textContent
) {
toggleSelected(parseInt(target.textContent), click); toggleSelected(parseInt(target.textContent), click);
} }
}; };
@@ -204,47 +289,3 @@ function useLineNumberMagic(
}; };
}, [editor, monaco, selected, forcedContent]); }, [editor, monaco, selected, forcedContent]);
} }
function useSelectedLine() {
// extract highlighted lines from window hash
const [selected, setSelected] = useState(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? start : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
let hash = '';
if (selected[0] !== -1) {
if (selected[1] !== selected[0]) {
const start = Math.min(...selected);
const end = Math.max(...selected);
hash = `#L${start}-${end}`;
} else {
hash = `#L${selected[0]}`;
}
}
history.replace({ hash });
}, [selected]);
// toggle the highlighting for a given line
function toggleSelected(lineNo, e) {
const shift = e.shiftKey;
if (selected[0] === lineNo && selected[1] === lineNo) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, lineNo]);
} else {
setSelected([selected[0], lineNo]);
}
}
return [selected, toggleSelected];
}

View File

@@ -1,25 +1,74 @@
import { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import Button from './Button';
export const Button = styled.div` interface MenuButtonProps<T extends string> {
cursor: pointer; label: string;
height: 100%; ids: T[] | Record<string, T[]>;
display: flex; value: T;
align-items: center; setValue: (value: T) => void;
padding: 0 0.25em;
color: inherit;
text-decoration: none;
:hover {
background: ${props => props.theme.highlight};
} }
@media (max-width: 640px) { export default function MenuButton<T extends string>({
span { label,
display: none; ids,
value,
setValue,
}: MenuButtonProps<T>) {
const [open, setOpen] = useState<boolean>(false);
// close the menu when a click is made elsewhere
useEffect(() => {
const listener = (e: MouseEvent) => setOpen(false);
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, [setOpen]);
function toggleOpen(e: React.MouseEvent) {
e.stopPropagation();
setOpen(!open);
} }
function select(e: React.MouseEvent<HTMLLIElement>, id: T) {
e.stopPropagation();
setOpen(false);
setValue(id);
}
function make(ids: T[]) {
return ids.map(id => (
<li
key={id}
className={id === value ? 'selected' : undefined}
onClick={e => select(e, id)}
>
{id}
</li>
));
}
let items;
if (Array.isArray(ids)) {
items = make(ids);
} else {
items = Object.entries(ids).map(([title, values]) =>
[
<li className="title" key={title} onClick={e => e.stopPropagation()}>
[{title}]
</li>,
]
.concat(make(values))
.flat()
);
}
return (
<Button onClick={toggleOpen}>
[<span>{label}: </span>
{value}]{open && <Menu>{items}</Menu>}
</Button>
);
} }
`;
const Menu = styled.ul` const Menu = styled.ul`
position: absolute; position: absolute;
@@ -60,58 +109,3 @@ const Menu = styled.ul`
background-color: ${props => props.theme.secondary}; background-color: ${props => props.theme.secondary};
} }
`; `;
export const MenuButton = ({ label, ids, value, setValue }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
const listener = e => setOpen(false);
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, [setOpen]);
function toggleOpen(e) {
e.stopPropagation();
setOpen(!open);
}
function select(e, id) {
e.stopPropagation();
setOpen(false);
setValue(id);
}
function make(ids) {
return ids.map(id => (
<li
key={id}
className={id === value ? 'selected' : undefined}
onClick={e => select(e, id)}
>
{id}
</li>
));
}
let items;
if (Array.isArray(ids)) {
items = make(ids);
} else {
items = Object.entries(ids).map(([title, values]) =>
[
<li className="title" key={title} onClick={e => e.stopPropagation()}>
[{title}]
</li>,
]
.concat(make(values))
.flat()
);
}
return (
<Button onClick={toggleOpen}>
[<span>{label}: </span>
{value}]{open && <Menu>{items}</Menu>}
</Button>
);
};

View File

@@ -0,0 +1,28 @@
import { get as lsGet, remove as lsRemove, set as lsSet } from 'local-storage';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
// hook used to load "preference" settings from local storage, or fall back to a default value.
export default function usePreference<T>(
id: string,
defaultValue: T,
valid: (value: T) => boolean
): [T, Dispatch<SetStateAction<T>>, (value: T) => boolean] {
const [value, setValue] = useState<T>(() => {
const pref = lsGet(id) as T;
if (pref && valid(pref)) {
return pref;
} else {
return defaultValue;
}
});
useEffect(() => {
if (value === defaultValue) {
lsRemove(id);
} else {
lsSet(id, value);
}
}, [value, id, defaultValue]);
return [value, setValue, valid];
}

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './style/base.css';
import App from './App'; import App from './App';
import './style/base.css';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>
); );

View File

@@ -1,4 +1,43 @@
const themes = { import type { editor } from 'monaco-editor';
export interface Theme {
id: string;
primary: string;
secondary: string;
highlight: string;
lightOrDark: string;
editor: {
background: string;
lineNumber: string;
lineNumberHl: string;
lineNumberHlBackground: string;
primary: string;
selection: string;
comment: string;
commentTag: string;
punctuation: string;
annotation: string;
namespace: string;
property: string;
constant: string;
number: string;
selector: string;
operator: string;
keyword: string;
function: string;
className: string;
variable: string;
};
}
export interface Themes {
light: Theme;
blue: Theme;
dark: Theme;
}
const themes: Themes = {
light: { light: {
id: 'light', id: 'light',
primary: '#aaddff', primary: '#aaddff',
@@ -93,7 +132,7 @@ const themes = {
export default themes; export default themes;
export function makeMonacoTheme(theme) { export function makeMonacoTheme(theme: Theme): editor.IStandaloneThemeData {
return { return {
base: theme.lightOrDark === 'light' ? 'vs' : 'vs-dark', base: theme.lightOrDark === 'light' ? 'vs' : 'vs-dark',
inherit: true, inherit: true,

83
src/util/storage.ts Normal file
View File

@@ -0,0 +1,83 @@
import { gzip } from 'pako';
import MIMEType from 'whatwg-mimetype';
import { bytebinUrl, postUrl } from './constants';
import { languageIds } from './highlighting';
interface LoadResultSuccess {
ok: true;
content: string;
type?: string;
}
interface LoadResultFail {
ok: false;
content?: never;
type?: never;
}
export type LoadResult = LoadResultSuccess | LoadResultFail;
export async function loadFromBytebin(id: string): Promise<LoadResult> {
try {
const resp = await fetch(bytebinUrl + id);
if (resp.ok) {
const content = await resp.text();
const type = contentTypeToLanguage(
resp.headers.get('content-type') as string
);
document.title = 'pastes | ' + id;
return { ok: true, content, type };
} else {
return { ok: false };
}
} catch (e) {
return { ok: false };
}
}
export async function saveToBytebin(
code: string,
language: string
): Promise<string | null> {
try {
const compressed = gzip(code);
const contentType = languageToContentType(language);
const resp = await fetch(postUrl, {
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Encoding': 'gzip',
'Accept': 'application/json',
},
body: compressed,
});
if (resp.ok) {
const json = await resp.json();
return json.key;
}
} catch (e) {
console.log(e);
}
return null;
}
export function contentTypeToLanguage(contentType: string) {
const { type, subtype: subType } = new MIMEType(contentType);
if (type === 'application' && subType === 'json') {
return 'json';
}
if (type === 'text' && languageIds.includes(subType.toLowerCase())) {
return subType.toLowerCase();
}
}
export function languageToContentType(language: string) {
if (language === 'json') {
return 'application/json';
} else {
return 'text/' + language;
}
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src/*.ts",
"src/*.tsx"
]
}

100
yarn.lock
View File

@@ -1565,10 +1565,10 @@
dependencies: dependencies:
state-local "^1.0.6" state-local "^1.0.6"
"@monaco-editor/react@^4.3.1": "@monaco-editor/react@4.4.6":
version "4.4.5" version "4.4.6"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.5.tgz#beabe491efeb2457441a00d1c7651c653697f65b" resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218"
integrity sha512-IImtzU7sRc66OOaQVCG+5PFHkSWnnhrUWGBuH6zNmH2h0YgmAhcjHZQc/6MY9JWEbUtVF1WPBMJ9u1XuFbRrVA== integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA==
dependencies: dependencies:
"@monaco-editor/loader" "^1.3.2" "@monaco-editor/loader" "^1.3.2"
prop-types "^15.7.2" prop-types "^15.7.2"
@@ -1961,6 +1961,14 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/hoist-non-react-statics@*":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/html-minifier-terser@^6.0.0": "@types/html-minifier-terser@^6.0.0":
version "6.1.0" version "6.1.0"
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
@@ -2000,6 +2008,14 @@
expect "^29.0.0" expect "^29.0.0"
pretty-format "^29.0.0" pretty-format "^29.0.0"
"@types/jest@^29.2.2":
version "29.2.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.2.2.tgz#874e7dc6702fa6a3fe6107792aa98636dcc480b4"
integrity sha512-og1wAmdxKoS71K2ZwSVqWPX6OVn3ihZ6ZT2qvZvZQm90lJVDyXIjYcu4Khx2CNIeaFv12rOU/YObOsI3VOkzog==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.11" version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@@ -2020,6 +2036,16 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9"
integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA== integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==
"@types/node@^18.11.9":
version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/pako@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.0.tgz#12ab4c19107528452e73ac99132c875ccd43bdfb"
integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==
"@types/parse-json@^4.0.0": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -2057,6 +2083,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-dom@^18.0.8":
version "18.0.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.8.tgz#d2606d855186cd42cc1b11e63a71c39525441685"
integrity sha512-C3GYO0HLaOkk9dDAz3Dl4sbe4AKUGTCfFIZsz3n/82dPNN8Du533HzKatDxeUYWu24wJgMP1xICqkWk1YOLOIw==
dependencies:
"@types/react" "*"
"@types/react@*": "@types/react@*":
version "18.0.18" version "18.0.18"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.18.tgz#9f16f33d57bc5d9dca848d12c3572110ff9429ac" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.18.tgz#9f16f33d57bc5d9dca848d12c3572110ff9429ac"
@@ -2066,6 +2099,15 @@
"@types/scheduler" "*" "@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@^18.0.25":
version "18.0.25"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44"
integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@1.17.1": "@types/resolve@1.17.1":
version "1.17.1" version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
@@ -2110,6 +2152,15 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/styled-components@^5.1.26":
version "5.1.26"
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.26.tgz#5627e6812ee96d755028a98dae61d28e57c233af"
integrity sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==
dependencies:
"@types/hoist-non-react-statics" "*"
"@types/react" "*"
csstype "^3.0.2"
"@types/testing-library__jest-dom@^5.9.1": "@types/testing-library__jest-dom@^5.9.1":
version "5.14.5" version "5.14.5"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz#d113709c90b3c75fdb127ec338dad7d5f86c974f"
@@ -2122,6 +2173,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/whatwg-mimetype@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#4ff45f787a085e7d22b7e01f3724f28a97d84c67"
integrity sha512-xHFOhd41VpUR6Y0k8ZinlyFv5cyhC/r2zghJgWWN8oNxqNo45Nf0qCBInJsFeifLeoHcIF4voEfap4A2GYHWkw==
"@types/ws@^8.5.1": "@types/ws@^8.5.1":
version "8.5.3" version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
@@ -3234,11 +3290,6 @@ content-disposition@0.5.4:
dependencies: dependencies:
safe-buffer "5.2.1" safe-buffer "5.2.1"
content-type-parser@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
integrity sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==
content-type@~1.0.4: content-type@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -4764,7 +4815,7 @@ history@^5.0.0:
dependencies: dependencies:
"@babel/runtime" "^7.7.6" "@babel/runtime" "^7.7.6"
hoist-non-react-statics@^3.0.0: hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6213,6 +6264,11 @@ mkdirp@~0.5.1:
dependencies: dependencies:
minimist "^1.2.6" minimist "^1.2.6"
monaco-editor@^0.34.1:
version "0.34.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87"
integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -7196,6 +7252,16 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
prettier-plugin-organize-imports@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.1.1.tgz#e581cb6fa528cf72f7d95b807ce38c3e51c3c27c"
integrity sha512-6bHIQzybqA644h0WGUW3gpWEVbMBvzui5wCMRBi7qA++d5ov2xjjfDk8pxJJ/ardfZrGAwizKMq/fQMFdJ+0Zw==
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -7389,7 +7455,7 @@ react-device-detect@^2.1.2:
dependencies: dependencies:
ua-parser-js "^1.0.2" ua-parser-js "^1.0.2"
react-dom@^18.1.0: react-dom@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@@ -7477,7 +7543,7 @@ react-scripts@5.0.1:
optionalDependencies: optionalDependencies:
fsevents "^2.3.2" fsevents "^2.3.2"
react@^18.1.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@@ -8547,6 +8613,11 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
ua-parser-js@^1.0.2: ua-parser-js@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
@@ -8871,6 +8942,11 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-mimetype@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
whatwg-url@^7.0.0: whatwg-url@^7.0.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"