From 736490082b756e650f380be1d8ad7589de1d0b77 Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 13:59:31 -0600 Subject: [PATCH 1/9] Add cssMinify Page --- pages/cssMinify.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pages/cssMinify.tsx diff --git a/pages/cssMinify.tsx b/pages/cssMinify.tsx new file mode 100644 index 0000000..c57ef7a --- /dev/null +++ b/pages/cssMinify.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import Heading from '../components/Heading'; +import Layout from '../components/Layout'; + +export default function CssMinifyPage() { + return ( + + Minify CSS + + ); +} From d8592580caed303d75cac7ddc2745d4b690d4fe1 Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 14:02:34 -0600 Subject: [PATCH 2/9] Add minifier to nav menu --- data/nav.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/nav.ts b/data/nav.ts index 9bb7c17..dcb07f3 100644 --- a/data/nav.ts +++ b/data/nav.ts @@ -1,5 +1,6 @@ import AspectRatio from '@mui/icons-material/AspectRatio'; import Code from '@mui/icons-material/Code'; +import Css from '@mui/icons-material/Css'; import DataObject from '@mui/icons-material/DataObject'; import Fingerprint from '@mui/icons-material/Fingerprint'; import HomeIcon from '@mui/icons-material/Home'; @@ -19,6 +20,11 @@ export default [ href: '/aspect-ratio', Icon: AspectRatio, }, + { + title: 'CSS Minifier', + href: '/cssMinify', + Icon: Css, + }, { title: 'UUID', href: '/uuid', From a5c8c2009e270515beea1ed2864019cb9a8ae60f Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 14:42:06 -0600 Subject: [PATCH 3/9] Add support for css formatting --- components/cssMinify/MinifyCss.tsx | 182 +++++++++++++++++++++++++++++ pages/cssMinify.tsx | 41 ++++++- 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 components/cssMinify/MinifyCss.tsx diff --git a/components/cssMinify/MinifyCss.tsx b/components/cssMinify/MinifyCss.tsx new file mode 100644 index 0000000..6a871de --- /dev/null +++ b/components/cssMinify/MinifyCss.tsx @@ -0,0 +1,182 @@ +import ClearIcon from '@mui/icons-material/Clear'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import prettier from 'prettier'; +import parserCss from 'prettier/parser-postcss'; +import React from 'react'; + +import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead'; +import type { ToastProps } from '../Toast'; + +export interface MinifyCssProps { + css: string | void; + formattedCss: string | void; + error: string; + setCss: (_css: string | void) => void; + setFormattedCss: (_formattedCss: string | void) => void; + setError: (_error: string) => void; + setToastMessage: (_toastMessage: string) => void; + setToastOpen: (_toastOpen: boolean) => void; + setToastSeverity: (_toastSeverity: ToastProps['severity']) => void; +} + +export default function MinifyCss({ + css, + formattedCss, + error, + setCss, + setFormattedCss, + setError, + setToastMessage, + setToastOpen, + setToastSeverity, +}: MinifyCssProps) { + const supportsClipboardRead = useSupportsClipboardRead(); + function calculateFormattedCss(value: string | void) { + setCss(value); + if (!value) { + setFormattedCss(''); + } + try { + const formatted = prettier.format(value ?? '', { + parser: 'css', + plugins: [parserCss], + }); + + setError(''); + setFormattedCss(formatted); + } catch (e: unknown) { + setError((e as { message: string })?.message ?? ''); + setFormattedCss(''); + } + } + + function handleChange(event: React.ChangeEvent) { + const { value, name } = event.target; + if (name === 'css') { + calculateFormattedCss(value); + } + } + + return ( + + + 0} + label='Css' + name='css' + onChange={handleChange} + value={css} + /> + + + {!!supportsClipboardRead && ( + + )} + + {error && ( +
+            
+              {error}
+            
+          
+ )} +
+ + + + + + + +
+ ); +} diff --git a/pages/cssMinify.tsx b/pages/cssMinify.tsx index c57ef7a..c7fb18d 100644 --- a/pages/cssMinify.tsx +++ b/pages/cssMinify.tsx @@ -1,12 +1,51 @@ -import React from 'react'; +import Typography from '@mui/material/Typography'; +import React, { useState } from 'react'; +import MinifyCss from '../components/cssMinify/MinifyCss'; import Heading from '../components/Heading'; import Layout from '../components/Layout'; +import Toast, { ToastProps } from '../components/Toast'; +import useLocalState from '../hooks/useLocalState'; export default function CssMinifyPage() { + const [css, setCss] = useLocalState({ + key: 'html', + defaultValue: '', + }); + const [formattedCss, setFormattedCss] = useLocalState({ + key: 'cssFormatted', + defaultValue: '', + }); + + const [error, setError] = React.useState(''); + const [toastOpen, setToastOpen] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastSeverity, setToastSeverity] = + useState('success'); + return ( Minify CSS + + Paste or type in some CSS to minify it + + + setToastOpen(false)} + /> ); } From fe7a02a6ddc3357b546b70a6577565fce36aa2c4 Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 14:45:52 -0600 Subject: [PATCH 4/9] Rename files --- components/{cssMinify/MinifyCss.tsx => css/CssForm.tsx} | 0 data/nav.ts | 4 ++-- pages/{cssMinify.tsx => css.tsx} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename components/{cssMinify/MinifyCss.tsx => css/CssForm.tsx} (100%) rename pages/{cssMinify.tsx => css.tsx} (91%) diff --git a/components/cssMinify/MinifyCss.tsx b/components/css/CssForm.tsx similarity index 100% rename from components/cssMinify/MinifyCss.tsx rename to components/css/CssForm.tsx diff --git a/data/nav.ts b/data/nav.ts index dcb07f3..a6f0aa6 100644 --- a/data/nav.ts +++ b/data/nav.ts @@ -21,8 +21,8 @@ export default [ Icon: AspectRatio, }, { - title: 'CSS Minifier', - href: '/cssMinify', + title: 'CSS', + href: '/css', Icon: Css, }, { diff --git a/pages/cssMinify.tsx b/pages/css.tsx similarity index 91% rename from pages/cssMinify.tsx rename to pages/css.tsx index c7fb18d..02df27b 100644 --- a/pages/cssMinify.tsx +++ b/pages/css.tsx @@ -1,7 +1,7 @@ import Typography from '@mui/material/Typography'; import React, { useState } from 'react'; -import MinifyCss from '../components/cssMinify/MinifyCss'; +import CssForm from '../components/css/CssForm'; import Heading from '../components/Heading'; import Layout from '../components/Layout'; import Toast, { ToastProps } from '../components/Toast'; @@ -27,9 +27,9 @@ export default function CssMinifyPage() { Minify CSS - Paste or type in some CSS to minify it + Paste or type in some CSS to format or minify it - Date: Wed, 5 Oct 2022 15:24:34 -0600 Subject: [PATCH 5/9] Add minify/format switch --- components/css/CssForm.tsx | 31 ++++++++++++++++++++++++++++--- pages/css.tsx | 8 +++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/components/css/CssForm.tsx b/components/css/CssForm.tsx index 6a871de..4f8acd2 100644 --- a/components/css/CssForm.tsx +++ b/components/css/CssForm.tsx @@ -3,6 +3,8 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import prettier from 'prettier'; @@ -12,29 +14,33 @@ import React from 'react'; import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead'; import type { ToastProps } from '../Toast'; -export interface MinifyCssProps { +export interface CssFormProps { css: string | void; formattedCss: string | void; error: string; + minifyCss: boolean; setCss: (_css: string | void) => void; setFormattedCss: (_formattedCss: string | void) => void; setError: (_error: string) => void; + setMinifyCss: (_minifyCss: boolean) => void; setToastMessage: (_toastMessage: string) => void; setToastOpen: (_toastOpen: boolean) => void; setToastSeverity: (_toastSeverity: ToastProps['severity']) => void; } -export default function MinifyCss({ +export default function CssForm({ css, formattedCss, error, + minifyCss, setCss, setFormattedCss, setError, + setMinifyCss, setToastMessage, setToastOpen, setToastSeverity, -}: MinifyCssProps) { +}: CssFormProps) { const supportsClipboardRead = useSupportsClipboardRead(); function calculateFormattedCss(value: string | void) { setCss(value); @@ -60,6 +66,9 @@ export default function MinifyCss({ if (name === 'css') { calculateFormattedCss(value); } + if (name === 'minifySwitch') { + setMinifyCss(!minifyCss); + } } return ( @@ -127,6 +136,22 @@ export default function MinifyCss({ )} + + Format + + + + Minify + ({ - key: 'html', + key: 'css', defaultValue: '', }); const [formattedCss, setFormattedCss] = useLocalState({ key: 'cssFormatted', defaultValue: '', }); + const [minifyCss, setMinifyCss] = useLocalState({ + key: 'cssMinify', + defaultValue: false, + }); const [error, setError] = React.useState(''); const [toastOpen, setToastOpen] = useState(false); @@ -33,9 +37,11 @@ export default function CssMinifyPage() { css={css} formattedCss={formattedCss} error={error} + minifyCss={minifyCss} setCss={setCss} setFormattedCss={setFormattedCss} setError={setError} + setMinifyCss={setMinifyCss} setToastMessage={setToastMessage} setToastOpen={setToastOpen} setToastSeverity={setToastSeverity} From d1a3a650e5cc1c994e727904872c4154c4c4da2f Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 15:58:03 -0600 Subject: [PATCH 6/9] Add minification support for css --- components/css/CssForm.tsx | 18 ++++++++++--- package-lock.json | 53 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + pages/css.tsx | 8 ++++-- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/components/css/CssForm.tsx b/components/css/CssForm.tsx index 4f8acd2..f96291e 100644 --- a/components/css/CssForm.tsx +++ b/components/css/CssForm.tsx @@ -7,6 +7,7 @@ import Grid from '@mui/material/Grid'; import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; +import { minify } from 'csso'; import prettier from 'prettier'; import parserCss from 'prettier/parser-postcss'; import React from 'react'; @@ -42,7 +43,10 @@ export default function CssForm({ setToastSeverity, }: CssFormProps) { const supportsClipboardRead = useSupportsClipboardRead(); - function calculateFormattedCss(value: string | void) { + function calculateFormattedCss( + value: string | void, + minification: boolean | false, + ) { setCss(value); if (!value) { setFormattedCss(''); @@ -53,8 +57,13 @@ export default function CssForm({ plugins: [parserCss], }); + if (minification) { + const minified = minify(formatted).css; + setFormattedCss(minified); + } else { + setFormattedCss(formatted); + } setError(''); - setFormattedCss(formatted); } catch (e: unknown) { setError((e as { message: string })?.message ?? ''); setFormattedCss(''); @@ -64,9 +73,10 @@ export default function CssForm({ function handleChange(event: React.ChangeEvent) { const { value, name } = event.target; if (name === 'css') { - calculateFormattedCss(value); + calculateFormattedCss(value, minifyCss); } if (name === 'minifySwitch') { + calculateFormattedCss(css, !minifyCss); setMinifyCss(!minifyCss); } } @@ -116,7 +126,7 @@ export default function CssForm({ const text = await navigator.clipboard.readText(); if (text) { setCss(text); - calculateFormattedCss(text); + calculateFormattedCss(text, minifyCss); } }} > diff --git a/package-lock.json b/package-lock.json index f9f0711..4a00465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/icons-material": "^5.10.3", "@mui/material": "^5.10.3", "clsx": "^1.2.1", + "csso": "^5.0.5", "js-base64": "^3.7.2", "next": "12.2.5", "next-images": "^1.8.4", @@ -4822,12 +4823,37 @@ "node": ">=8" } }, + "node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -8470,6 +8496,11 @@ "tmpl": "1.0.5" } }, + "node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -14524,12 +14555,29 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, + "css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "requires": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + } + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "requires": { + "css-tree": "~2.2.0" + } + }, "cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -17194,6 +17242,11 @@ "tmpl": "1.0.5" } }, + "mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 6a2921c..d238e4f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mui/icons-material": "^5.10.3", "@mui/material": "^5.10.3", "clsx": "^1.2.1", + "csso": "^5.0.5", "js-base64": "^3.7.2", "next": "12.2.5", "next-images": "^1.8.4", diff --git a/pages/css.tsx b/pages/css.tsx index 0b5701c..60a6532 100644 --- a/pages/css.tsx +++ b/pages/css.tsx @@ -7,7 +7,7 @@ import Layout from '../components/Layout'; import Toast, { ToastProps } from '../components/Toast'; import useLocalState from '../hooks/useLocalState'; -export default function CssMinifyPage() { +export default function CssPage() { const [css, setCss] = useLocalState({ key: 'css', defaultValue: '', @@ -31,7 +31,11 @@ export default function CssMinifyPage() { Minify CSS - Paste or type in some CSS to format or minify it + Paste or type in some CSS to format or minify it. + + + Note: Formatting works with CSS, SCSS, and LESS. Minification + only works for raw CSS. Date: Wed, 5 Oct 2022 16:07:53 -0600 Subject: [PATCH 7/9] Change labels for CSS textfields --- components/css/CssForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/css/CssForm.tsx b/components/css/CssForm.tsx index f96291e..3d0b9cc 100644 --- a/components/css/CssForm.tsx +++ b/components/css/CssForm.tsx @@ -98,7 +98,7 @@ export default function CssForm({ 0} - label='Css' + label='CSS' name='css' onChange={handleChange} value={css} @@ -169,8 +169,8 @@ export default function CssForm({ > Date: Wed, 5 Oct 2022 16:32:35 -0600 Subject: [PATCH 8/9] Add tests for CSS --- __TESTS__/css.spec.tsx | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 __TESTS__/css.spec.tsx diff --git a/__TESTS__/css.spec.tsx b/__TESTS__/css.spec.tsx new file mode 100644 index 0000000..cf589d2 --- /dev/null +++ b/__TESTS__/css.spec.tsx @@ -0,0 +1,49 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +/* eslint-enable import/no-extraneous-dependencies */ +import React from 'react'; + +import Css from '../pages/css'; + +describe('CSS', () => { + it('formats CSS and adds newlines', async () => { + const user = userEvent.setup(); + render(); + + const css = screen.getByLabelText(/^CSS$/i); + const formattedCss = screen.getByLabelText(/FormattedCSS/i); + + await user.clear(css); + await user.type( + css, + 'body{background:#ffffff;}'.replace(/[{[]/g, '$&$&'), + ); + expect(formattedCss.innerHTML).toContain('body {'); + expect(formattedCss.innerHTML).toContain('background: #ffffff;'); + expect(formattedCss.innerHTML).toContain('\n'); + }); + + it('clears inputs with either clear button', async () => { + const user = userEvent.setup(); + render(); + + const css = screen.getByLabelText(/^CSS$/i); + const formattedCss = screen.getByLabelText(/FormattedCSS/i); + + const clearBtns = screen.getAllByRole('button', { name: /Clear/i }); + + await user.clear(css); + await user.type(css, 'body{}'.replace(/[{[]/g, '$&$&')); + await user.click(clearBtns[0]); + + expect(css).toHaveValue(''); + expect(formattedCss).toHaveValue(''); + + await user.type(css, 'body{}'.replace(/[{[]/g, '$&$&')); + await user.click(clearBtns[1]); + + expect(css).toHaveValue(''); + expect(formattedCss).toHaveValue(''); + }); +}); From f1c2189db0bc1d8ba02d7a1b508926af083a265a Mon Sep 17 00:00:00 2001 From: Dustin Lammiman Date: Wed, 5 Oct 2022 18:07:11 -0600 Subject: [PATCH 9/9] Improve tests --- __TESTS__/css.spec.tsx | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/__TESTS__/css.spec.tsx b/__TESTS__/css.spec.tsx index cf589d2..0bab19d 100644 --- a/__TESTS__/css.spec.tsx +++ b/__TESTS__/css.spec.tsx @@ -7,7 +7,7 @@ import React from 'react'; import Css from '../pages/css'; describe('CSS', () => { - it('formats CSS and adds newlines', async () => { + it('formats CSS correctly', async () => { const user = userEvent.setup(); render(); @@ -19,9 +19,35 @@ describe('CSS', () => { css, 'body{background:#ffffff;}'.replace(/[{[]/g, '$&$&'), ); - expect(formattedCss.innerHTML).toContain('body {'); - expect(formattedCss.innerHTML).toContain('background: #ffffff;'); - expect(formattedCss.innerHTML).toContain('\n'); + expect(formattedCss.innerHTML).toBe(`body { + background: #ffffff; +} +`); + }); + + it('minifies CSS correctly', async () => { + const user = userEvent.setup(); + render(); + + const css = screen.getByLabelText(/^CSS$/i); + const minifySwitch = screen.getByRole('checkbox', { + name: /Minify/i, + }); + + await user.clear(css); + await user.type( + css, + `body{ + background:#ffffff; + }`.replace(/[{[]/g, '$&$&'), + ); + + await user.click(minifySwitch); + const minifiedCss = screen.getByLabelText(/MinifiedCSS/i); + + expect(minifiedCss.innerHTML).toBe('body{background:#fff}'); + + await user.click(minifySwitch); }); it('clears inputs with either clear button', async () => {