Merge pull request #148 from sambecker/html-description

Rich about text formatting
This commit is contained in:
Sam Becker 2024-09-21 14:24:49 -05:00 committed by GitHub
commit 57c9b30e35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 114 additions and 6 deletions

View File

@ -98,8 +98,8 @@ Application behavior can be changed by configuring the following environment var
#### Site meta
- `NEXT_PUBLIC_SITE_TITLE` (seen in browser tab)
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, under title)
- `NEXT_PUBLIC_SITE_ABOUT` (e.g., seen in grid sidebar)
- `NEXT_PUBLIC_SITE_DESCRIPTION` (seen in nav, beneath title)
- `NEXT_PUBLIC_SITE_ABOUT` (seen in grid sidebar—accepted rich formatting tags: `<b>`, `<strong>`, `<i>`, `<em>`, `<u>`, `<br>`)
#### Site behavior
- `NEXT_PUBLIC_GRID_HOMEPAGE = 1` shows grid layout on homepage

17
__tests__/html.test.ts Normal file
View File

@ -0,0 +1,17 @@
import { htmlHasBrParagraphBreaks, safelyParseFormattedHtml } from '@/utility/html';
import { parameterize } from '@/utility/string';
describe('HTML', () => {
it('safely parses', () => {
expect(safelyParseFormattedHtml('<p>TEXT</p>')).toBe('TEXT');
expect(safelyParseFormattedHtml('<b>TEXT</b>')).toBe('<b>TEXT</b>');
});
it('detects br-style paragraph breaks', () => {
expect(htmlHasBrParagraphBreaks('TEXT<br><br>')).toBeTruthy();
expect(htmlHasBrParagraphBreaks('TEXT<br /><br />')).toBeTruthy();
expect(htmlHasBrParagraphBreaks('TEXT<br><br />')).toBeTruthy();
expect(htmlHasBrParagraphBreaks('TEXT')).toBeFalsy();
expect(htmlHasBrParagraphBreaks('TEXT<br/>')).toBeFalsy();
expect(htmlHasBrParagraphBreaks('TEXT<br />')).toBeFalsy();
});
});

View File

@ -23,6 +23,7 @@
"@types/pg": "^8.11.10",
"@types/react": "18.3.8",
"@types/react-dom": "18.3.0",
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@upstash/ratelimit": "^2.0.3",
@ -51,6 +52,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-icons": "^5.3.0",
"sanitize-html": "^2.13.0",
"sharp": "^0.33.5",
"sonner": "^1.5.0",
"swr": "^2.2.5",

71
pnpm-lock.yaml generated
View File

@ -50,6 +50,9 @@ importers:
'@types/react-dom':
specifier: 18.3.0
version: 18.3.0
'@types/sanitize-html':
specifier: ^2.13.0
version: 2.13.0
'@typescript-eslint/eslint-plugin':
specifier: ^7.17.0
version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)
@ -134,6 +137,9 @@ importers:
react-icons:
specifier: ^5.3.0
version: 5.3.0(react@18.3.1)
sanitize-html:
specifier: ^2.13.0
version: 2.13.0
sharp:
specifier: ^0.33.5
version: 0.33.5
@ -1677,6 +1683,9 @@ packages:
'@types/react@18.3.8':
resolution: {integrity: sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==}
'@types/sanitize-html@2.13.0':
resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==}
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@ -2354,11 +2363,24 @@ packages:
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domexception@4.0.0:
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
engines: {node: '>=12'}
deprecated: Use your platform's native DOMException instead
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@ -2785,6 +2807,9 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
@ -3521,6 +3546,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-srcset@1.0.2:
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
@ -3897,6 +3925,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sanitize-html@2.13.0:
resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@ -6514,6 +6545,10 @@ snapshots:
'@types/prop-types': 15.7.12
csstype: 3.1.3
'@types/sanitize-html@2.13.0':
dependencies:
htmlparser2: 8.0.2
'@types/stack-utils@2.0.3': {}
'@types/tough-cookie@4.0.5': {}
@ -7262,10 +7297,28 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domexception@4.0.0:
dependencies:
webidl-conversions: 7.0.0
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
domutils@3.1.0:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
@ -7842,6 +7895,13 @@ snapshots:
html-escaper@2.0.2: {}
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
http-proxy-agent@5.0.0:
dependencies:
'@tootallnate/once': 2.0.0
@ -8744,6 +8804,8 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-srcset@1.0.2: {}
parse5@7.1.2:
dependencies:
entities: 4.5.0
@ -9091,6 +9153,15 @@ snapshots:
safer-buffer@2.1.2: {}
sanitize-html@2.13.0:
dependencies:
deepmerge: 4.3.1
escape-string-regexp: 4.0.0
htmlparser2: 8.0.2
is-plain-object: 5.0.0
parse-srcset: 1.0.2
postcss: 8.4.47
sax@1.2.4: {}
saxes@6.0.0:

View File

@ -16,6 +16,8 @@ import { useAppState } from '@/state/AppState';
import { useMemo } from 'react';
import HiddenTag from '@/tag/HiddenTag';
import { SITE_ABOUT } from '@/site/config';
import { htmlHasBrParagraphBreaks, safelyParseFormattedHtml } from '@/utility/html';
import { clsx } from 'clsx/lite';
export default function PhotoGridSidebar({
tags,
@ -43,10 +45,14 @@ export default function PhotoGridSidebar({
{SITE_ABOUT && <HeaderList
items={[<p
key="about"
className="max-w-60 normal-case text-main"
>
{SITE_ABOUT}
</p>]}
className={clsx(
'max-w-60 normal-case text-main',
htmlHasBrParagraphBreaks(SITE_ABOUT) && 'pb-2',
)}
dangerouslySetInnerHTML={{
__html: safelyParseFormattedHtml(SITE_ABOUT),
}}
/>]}
/>}
{tags.length > 0 && <HeaderList
title='Tags'

12
src/utility/html.ts Normal file
View File

@ -0,0 +1,12 @@
import sanitizeHtml from 'sanitize-html';
const ALLOWED_FORMATTING_TAGS = ['b', 'strong', 'i', 'em', 'u', 'br'];
export const safelyParseFormattedHtml = (text: string) =>
sanitizeHtml(text, {
allowedTags: ALLOWED_FORMATTING_TAGS,
});
// Matches two or more <br> or <br /> tags in a row
export const htmlHasBrParagraphBreaks = (text: string) =>
text.match(/(<br\s*\/?>){2}/i);