From 14cc7016b9da817fe1b3833e99ce4ff446cd2d51 Mon Sep 17 00:00:00 2001 From: Michael Varrieur Date: Mon, 3 Oct 2022 20:44:05 -0400 Subject: [PATCH 1/4] Add HTML formatter component --- __TESTS__/html.spec.tsx | 45 +++++++++ components/html/HtmlFormat.tsx | 170 +++++++++++++++++++++++++++++++++ data/nav.ts | 6 ++ package-lock.json | 7 +- package.json | 1 + pages/html.tsx | 54 +++++++++++ 6 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 __TESTS__/html.spec.tsx create mode 100644 components/html/HtmlFormat.tsx create mode 100644 pages/html.tsx diff --git a/__TESTS__/html.spec.tsx b/__TESTS__/html.spec.tsx new file mode 100644 index 0000000..95188c6 --- /dev/null +++ b/__TESTS__/html.spec.tsx @@ -0,0 +1,45 @@ +/* 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 Html from '../pages/html'; + +describe('HTML', () => { + it('formats HTML and adds newlines', async () => { + const user = userEvent.setup(); + render(); + + const html = screen.getByLabelText(/^HTML$/i); + const formattedHtml = screen.getByLabelText(/FormattedHTML/i); + + await user.clear(html); + await user.type(html, ''); + expect(formattedHtml.innerHTML).toContain('</html>'); + expect(formattedHtml.innerHTML).toContain('\n'); + }); + + it('clears inputs with either clear button', async () => { + const user = userEvent.setup(); + render(); + + const html = screen.getByLabelText(/^HTML$/i); + const formattedHtml = screen.getByLabelText(/FormattedHTML/i); + + const clearBtns = screen.getAllByRole('button', { name: /Clear/i }); + + await user.clear(html); + await user.type(html, ''); + await user.click(clearBtns[0]); + + expect(html).toHaveValue(''); + expect(formattedHtml).toHaveValue(''); + + await user.type(html, ''); + await user.click(clearBtns[1]); + + expect(html).toHaveValue(''); + expect(formattedHtml).toHaveValue(''); + }); +}); diff --git a/components/html/HtmlFormat.tsx b/components/html/HtmlFormat.tsx new file mode 100644 index 0000000..b5558be --- /dev/null +++ b/components/html/HtmlFormat.tsx @@ -0,0 +1,170 @@ +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 prettier from 'prettier'; +import parserHtml from 'prettier/parser-html'; +import React from 'react'; + +import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead'; +import type { ToastProps } from '../Toast'; + +export interface HtmlFormatProps { + html: string | void; + formattedHtml: string | void; + isError: boolean; + setHtml: (_html: string | void) => void; + setFormattedHtml: (_formattedHtml: string | void) => void; + setIsError: (_isError: boolean) => void; + setToastMessage: (_toastMessage: string) => void; + setToastOpen: (_toastOpen: boolean) => void; + setToastSeverity: (_toastSeverity: ToastProps['severity']) => void; +} + +export default function HtmlFormat({ + html, + formattedHtml, + isError, + setHtml, + setFormattedHtml, + setIsError, + setToastMessage, + setToastOpen, + setToastSeverity, +}: HtmlFormatProps) { + const supportsClipboardRead = useSupportsClipboardRead(); + function calculateFormattedHtml(value: string | void) { + setHtml(value); + if (!value) { + setFormattedHtml(''); + } + try { + const formatted = prettier.format(value ?? '', { + parser: 'html', + plugins: [parserHtml], + }); + + setIsError(false); + setFormattedHtml(formatted); + } catch (e) { + setIsError(true); + setFormattedHtml(''); + } + } + + function handleChange(event: React.ChangeEvent) { + const { value, name } = event.target; + if (name === 'html') { + calculateFormattedHtml(value); + } + } + + return ( + + + + + + {!!supportsClipboardRead && ( + + )} + + + + + + + + + + + ); +} diff --git a/data/nav.ts b/data/nav.ts index bbfad25..9bb7c17 100644 --- a/data/nav.ts +++ b/data/nav.ts @@ -1,4 +1,5 @@ import AspectRatio from '@mui/icons-material/AspectRatio'; +import Code from '@mui/icons-material/Code'; import DataObject from '@mui/icons-material/DataObject'; import Fingerprint from '@mui/icons-material/Fingerprint'; import HomeIcon from '@mui/icons-material/Home'; @@ -38,4 +39,9 @@ export default [ href: '/urlEncodeDecode', Icon: LinkIcon, }, + { + title: 'HTML', + href: '/html', + Icon: Code, + }, ]; diff --git a/package-lock.json b/package-lock.json index 07eaa1a..f9f0711 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "next": "12.2.5", "next-images": "^1.8.4", "next-pwa": "^5.6.0", + "prettier": "^2.7.1", "react": "18.2.0", "react-dom": "18.2.0", "uuid": "^9.0.0" @@ -9150,8 +9151,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -17646,9 +17645,7 @@ "prettier": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "peer": true + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==" }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/package.json b/package.json index b42ba48..6a2921c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "next": "12.2.5", "next-images": "^1.8.4", "next-pwa": "^5.6.0", + "prettier": "^2.7.1", "react": "18.2.0", "react-dom": "18.2.0", "uuid": "^9.0.0" diff --git a/pages/html.tsx b/pages/html.tsx new file mode 100644 index 0000000..29f6e6b --- /dev/null +++ b/pages/html.tsx @@ -0,0 +1,54 @@ +import Typography from '@mui/material/Typography'; +import React, { useState } from 'react'; + +import Heading from '../components/Heading'; +import HtmlFormat from '../components/html/HtmlFormat'; +import Layout from '../components/Layout'; +import Toast, { ToastProps } from '../components/Toast'; +import useLocalState from '../hooks/useLocalState'; + +export default function HtmlPage() { + const [html, setHtml] = useLocalState({ + key: 'html', + defaultValue: '', + }); + + const [formattedHtml, setFormattedHtml] = useLocalState< + string | void + >({ + key: 'htmlFormatted', + defaultValue: '', + }); + + const [isError, setIsError] = React.useState(false); + const [toastOpen, setToastOpen] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastSeverity, setToastSeverity] = + useState('success'); + + return ( + + HTML + + Paste or type in some unformatted HTML to format it + + + setToastOpen(false)} + /> + + ); +} From 61f68f5bafa2d796b7a0eecf912d72d5b7b4dcf2 Mon Sep 17 00:00:00 2001 From: Michael Varrieur Date: Tue, 4 Oct 2022 17:48:13 -0400 Subject: [PATCH 2/4] Add error message to page --- components/html/HtmlFormat.tsx | 24 +++++++++++++++++------- pages/html.tsx | 6 +++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/components/html/HtmlFormat.tsx b/components/html/HtmlFormat.tsx index b5558be..b268675 100644 --- a/components/html/HtmlFormat.tsx +++ b/components/html/HtmlFormat.tsx @@ -4,6 +4,7 @@ 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 parserHtml from 'prettier/parser-html'; import React from 'react'; @@ -14,10 +15,10 @@ import type { ToastProps } from '../Toast'; export interface HtmlFormatProps { html: string | void; formattedHtml: string | void; - isError: boolean; + error: string; setHtml: (_html: string | void) => void; setFormattedHtml: (_formattedHtml: string | void) => void; - setIsError: (_isError: boolean) => void; + setError: (_error: string) => void; setToastMessage: (_toastMessage: string) => void; setToastOpen: (_toastOpen: boolean) => void; setToastSeverity: (_toastSeverity: ToastProps['severity']) => void; @@ -26,10 +27,10 @@ export interface HtmlFormatProps { export default function HtmlFormat({ html, formattedHtml, - isError, + error, setHtml, setFormattedHtml, - setIsError, + setError, setToastMessage, setToastOpen, setToastSeverity, @@ -46,10 +47,10 @@ export default function HtmlFormat({ plugins: [parserHtml], }); - setIsError(false); + setError(''); setFormattedHtml(formatted); } catch (e) { - setIsError(true); + setError(e.message); setFormattedHtml(''); } } @@ -77,6 +78,7 @@ export default function HtmlFormat({ > 0} label='HTML' name='html' onChange={handleChange} @@ -113,6 +115,15 @@ export default function HtmlFormat({ )} + {error && ( + +
{error}
+
+ )} (false); const [toastMessage, setToastMessage] = useState(''); const [toastSeverity, setToastSeverity] = @@ -35,10 +35,10 @@ export default function HtmlPage() { Date: Tue, 4 Oct 2022 17:55:00 -0400 Subject: [PATCH 3/4] Fix TS error --- components/html/HtmlFormat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/html/HtmlFormat.tsx b/components/html/HtmlFormat.tsx index b268675..3134da5 100644 --- a/components/html/HtmlFormat.tsx +++ b/components/html/HtmlFormat.tsx @@ -49,8 +49,8 @@ export default function HtmlFormat({ setError(''); setFormattedHtml(formatted); - } catch (e) { - setError(e.message); + } catch (e: unknown) { + setError((e as { message: string })?.message ?? ''); setFormattedHtml(''); } } From f24d47d1241c13330e89e337584812ecb3441e96 Mon Sep 17 00:00:00 2001 From: Michael Varrieur Date: Tue, 4 Oct 2022 18:00:07 -0400 Subject: [PATCH 4/4] Move pre tag outside of p tag --- components/html/HtmlFormat.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/html/HtmlFormat.tsx b/components/html/HtmlFormat.tsx index 3134da5..0621f10 100644 --- a/components/html/HtmlFormat.tsx +++ b/components/html/HtmlFormat.tsx @@ -116,13 +116,15 @@ export default function HtmlFormat({ )} {error && ( - -
{error}
-
+
+            
+              {error}
+            
+          
)}