feat(chat): support line breaks and pasted content. Closes #3108
This commit is contained in:
parent
bd6e263eb9
commit
a354787a9e
@ -149,7 +149,7 @@ func sanitize(raw string) string {
|
|||||||
// Allow breaks
|
// Allow breaks
|
||||||
p.AllowElements("br")
|
p.AllowElements("br")
|
||||||
|
|
||||||
p.AllowElementsContent("p")
|
p.AllowElements("p")
|
||||||
|
|
||||||
// Allow img tags from the the local emoji directory only
|
// Allow img tags from the the local emoji directory only
|
||||||
p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img")
|
p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img")
|
||||||
|
@ -16,11 +16,11 @@ Here is an iframe<iframe src="http://yahoo.com"></iframe>
|
|||||||
[test link](http://owncast.online)
|
[test link](http://owncast.online)
|
||||||
<img class="emoji" src="/img/emoji/bananadance.gif">`
|
<img class="emoji" src="/img/emoji/bananadance.gif">`
|
||||||
|
|
||||||
expected := `Test one two three! I go to <a href="http://yahoo.com" rel="nofollow noreferrer noopener" target="_blank">http://yahoo.com</a> and search for <em>sports</em> and <strong>answers</strong>.
|
expected := `<p>Test one two three! I go to <a href="http://yahoo.com" rel="nofollow noreferrer noopener" target="_blank">http://yahoo.com</a> and search for <em>sports</em> and <strong>answers</strong>.
|
||||||
Here is an iframe
|
Here is an iframe</p>
|
||||||
blah blah blah
|
blah blah blah
|
||||||
<a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
|
<p><a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
|
||||||
<img class="emoji" src="/img/emoji/bananadance.gif">`
|
<img class="emoji" src="/img/emoji/bananadance.gif"></p>`
|
||||||
|
|
||||||
result := events.RenderAndSanitize(messageContent)
|
result := events.RenderAndSanitize(messageContent)
|
||||||
if result != expected {
|
if result != expected {
|
||||||
@ -31,7 +31,7 @@ blah blah blah
|
|||||||
// Test to make sure we block remote images in chat messages.
|
// Test to make sure we block remote images in chat messages.
|
||||||
func TestBlockRemoteImages(t *testing.T) {
|
func TestBlockRemoteImages(t *testing.T) {
|
||||||
messageContent := `<img src="https://via.placeholder.com/img/emoji/350x150"> test ![](https://via.placeholder.com/img/emoji/350x150)`
|
messageContent := `<img src="https://via.placeholder.com/img/emoji/350x150"> test ![](https://via.placeholder.com/img/emoji/350x150)`
|
||||||
expected := `test`
|
expected := `<p> test </p>`
|
||||||
result := events.RenderAndSanitize(messageContent)
|
result := events.RenderAndSanitize(messageContent)
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
@ -42,7 +42,7 @@ func TestBlockRemoteImages(t *testing.T) {
|
|||||||
// Test to make sure emoji images are allowed in chat messages.
|
// Test to make sure emoji images are allowed in chat messages.
|
||||||
func TestAllowEmojiImages(t *testing.T) {
|
func TestAllowEmojiImages(t *testing.T) {
|
||||||
messageContent := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test ![](/img/emoji/beerparrot.gif)`
|
messageContent := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test ![](/img/emoji/beerparrot.gif)`
|
||||||
expected := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test <img src="/img/emoji/beerparrot.gif">`
|
expected := `<p><img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test <img src="/img/emoji/beerparrot.gif"></p>`
|
||||||
result := events.RenderAndSanitize(messageContent)
|
result := events.RenderAndSanitize(messageContent)
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "stylelint-config-standard-scss",
|
"extends": "stylelint-config-standard-scss",
|
||||||
"rules": {
|
"rules": {
|
||||||
"selector-class-pattern": null
|
"selector-class-pattern": null,
|
||||||
|
"no-descending-specificity": null
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import React, { FC, useReducer, useRef, useState } from 'react';
|
import React, { FC, useEffect, useReducer, useRef, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import ContentEditable from 'react-contenteditable';
|
import ContentEditable from 'react-contenteditable';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import WebsocketService from '../../../services/websocket-service';
|
import WebsocketService from '../../../services/websocket-service';
|
||||||
@ -64,43 +66,6 @@ function setCaretPosition(editableDiv, position) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToText(str = '') {
|
|
||||||
// Ensure string.
|
|
||||||
let value = String(str);
|
|
||||||
|
|
||||||
// Convert encoding.
|
|
||||||
value = value.replace(/ /gi, ' ');
|
|
||||||
value = value.replace(/&/gi, '&');
|
|
||||||
|
|
||||||
// Replace `<br>`.
|
|
||||||
value = value.replace(/<br>/gi, '\n');
|
|
||||||
|
|
||||||
// Replace `<div>` (from Chrome).
|
|
||||||
value = value.replace(/<div>/gi, '\n');
|
|
||||||
|
|
||||||
// Replace `<p>` (from IE).
|
|
||||||
value = value.replace(/<p>/gi, '\n');
|
|
||||||
|
|
||||||
// Cleanup the emoji titles.
|
|
||||||
value = value.replace(/\u200C{2}/gi, '');
|
|
||||||
|
|
||||||
// Trim each line.
|
|
||||||
value = value
|
|
||||||
.split('\n')
|
|
||||||
.map((line = '') => line.trim())
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// No more than 2x newline, per "paragraph".
|
|
||||||
value = value.replace(/\n\n+/g, '\n\n');
|
|
||||||
|
|
||||||
// Clean up spaces.
|
|
||||||
value = value.replace(/[ ]+/g, ' ');
|
|
||||||
value = value.trim();
|
|
||||||
|
|
||||||
// Expose string.
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||||
@ -132,44 +97,21 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
};
|
};
|
||||||
|
|
||||||
const insertTextAtCursor = (textToInsert: string) => {
|
const insertTextAtCursor = (textToInsert: string) => {
|
||||||
const output = [
|
let cursorLocation;
|
||||||
text.current.slice(0, savedCursorLocation),
|
if (savedCursorLocation > 0) {
|
||||||
textToInsert,
|
cursorLocation = savedCursorLocation;
|
||||||
text.current.slice(savedCursorLocation),
|
} else {
|
||||||
].join('');
|
cursorLocation = getCaretPosition(document.getElementById('chat-input'));
|
||||||
text.current = output;
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertOnPaste = (event: React.ClipboardEvent) => {
|
|
||||||
// Prevent paste.
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// Set later.
|
|
||||||
let value = '';
|
|
||||||
|
|
||||||
// Does method exist?
|
|
||||||
const hasEventClipboard = !!(
|
|
||||||
event.clipboardData &&
|
|
||||||
typeof event.clipboardData === 'object' &&
|
|
||||||
typeof event.clipboardData.getData === 'function'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get clipboard data?
|
|
||||||
if (hasEventClipboard) {
|
|
||||||
value = event.clipboardData.getData('text/plain');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert into temp `<textarea>`, read back out.
|
const output = [
|
||||||
const textarea = document.createElement('textarea');
|
text.current.slice(0, cursorLocation),
|
||||||
textarea.innerHTML = value;
|
textToInsert,
|
||||||
value = textarea.innerText;
|
text.current.slice(cursorLocation),
|
||||||
|
].join('');
|
||||||
|
|
||||||
// Clean up text.
|
text.current = output;
|
||||||
value = convertToText(value);
|
forceUpdate();
|
||||||
|
|
||||||
// Insert text.
|
|
||||||
insertTextAtCursor(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Native emoji
|
// Native emoji
|
||||||
@ -184,14 +126,13 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
const charCount = getCharacterCount() + 1;
|
// Allow native line breaks
|
||||||
|
if (e.key === 'Enter' && e.shiftKey) {
|
||||||
// Send the message when hitting enter.
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
sendMessage();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const charCount = getCharacterCount() + 1;
|
||||||
|
|
||||||
// Always allow backspace.
|
// Always allow backspace.
|
||||||
if (e.key === 'Backspace') {
|
if (e.key === 'Backspace') {
|
||||||
setCharacterCount(charCount - 1);
|
setCharacterCount(charCount - 1);
|
||||||
@ -213,11 +154,36 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
if (charCount + 1 > characterLimit) {
|
if (charCount + 1 > characterLimit) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send the message when hitting enter.
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setCharacterCount(charCount + 1);
|
setCharacterCount(charCount + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = evt => {
|
const handleChange = evt => {
|
||||||
text.current = evt.target.value;
|
const sanitized = sanitizeHtml(evt.target.value, {
|
||||||
|
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'br', 'p', 'img'],
|
||||||
|
allowedAttributes: {
|
||||||
|
img: ['class', 'alt', 'title', 'src'],
|
||||||
|
},
|
||||||
|
allowedClasses: {
|
||||||
|
img: ['emoji'],
|
||||||
|
},
|
||||||
|
transformTags: {
|
||||||
|
h1: 'p',
|
||||||
|
h2: 'p',
|
||||||
|
h3: 'p',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
text.current = sanitized;
|
||||||
|
setSavedCursorLocation(
|
||||||
|
getCaretPosition(document.getElementById('chat-input-content-editable')),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
@ -237,6 +203,14 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
setSavedCursorLocation(0);
|
setSavedCursorLocation(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Focus the input when the component mounts.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!focusInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('chat-input-content-editable').focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="chat-input" className={styles.root}>
|
<div id="chat-input" className={styles.root}>
|
||||||
<div
|
<div
|
||||||
@ -260,11 +234,9 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
placeholder={enabled ? 'Send a message to chat' : 'Chat is disabled'}
|
placeholder={enabled ? 'Send a message to chat' : 'Chat is disabled'}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onPaste={convertOnPaste}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
autoFocus={focusInput}
|
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
role="textbox"
|
role="textbox"
|
||||||
aria-label="Chat text input"
|
aria-label="Chat text input"
|
||||||
|
@ -8,6 +8,12 @@ $p-v-size: 2px;
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chat messages are wrapped in <p> tags. We don't want to render
|
||||||
|
// the default margins for these initial <p> tags, so we remove them here.
|
||||||
|
p:nth-of-type(1) {
|
||||||
|
margin: initial;
|
||||||
|
}
|
||||||
|
|
||||||
border-left: $border-style;
|
border-left: $border-style;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: var(--chat-message-text-size);
|
font-size: var(--chat-message-text-size);
|
||||||
|
170
web/package-lock.json
generated
170
web/package-lock.json
generated
@ -43,6 +43,7 @@
|
|||||||
"react-markdown": "8.0.7",
|
"react-markdown": "8.0.7",
|
||||||
"react-virtuoso": "4.3.11",
|
"react-virtuoso": "4.3.11",
|
||||||
"recoil": "0.7.7",
|
"recoil": "0.7.7",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"ua-parser-js": "1.0.35",
|
"ua-parser-js": "1.0.35",
|
||||||
"video.js": "^8.3.0",
|
"video.js": "^8.3.0",
|
||||||
@ -77,6 +78,7 @@
|
|||||||
"@types/prop-types": "15.7.5",
|
"@types/prop-types": "15.7.5",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
"@types/react-linkify": "1.0.1",
|
"@types/react-linkify": "1.0.1",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@types/video.js": "^7.3.50",
|
"@types/video.js": "^7.3.50",
|
||||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||||
@ -13549,6 +13551,77 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sanitize-html": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"htmlparser2": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
@ -19997,7 +20070,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -20311,7 +20383,6 @@
|
|||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
},
|
},
|
||||||
@ -36045,6 +36116,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||||
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||||
@ -39736,6 +39812,96 @@
|
|||||||
"which": "bin/which"
|
"which": "bin/which"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^8.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.63.6",
|
"version": "1.63.6",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"react-markdown": "8.0.7",
|
"react-markdown": "8.0.7",
|
||||||
"react-virtuoso": "4.3.11",
|
"react-virtuoso": "4.3.11",
|
||||||
"recoil": "0.7.7",
|
"recoil": "0.7.7",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"ua-parser-js": "1.0.35",
|
"ua-parser-js": "1.0.35",
|
||||||
"video.js": "^8.3.0",
|
"video.js": "^8.3.0",
|
||||||
@ -82,6 +83,7 @@
|
|||||||
"@types/prop-types": "15.7.5",
|
"@types/prop-types": "15.7.5",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
"@types/react-linkify": "1.0.1",
|
"@types/react-linkify": "1.0.1",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@types/video.js": "^7.3.50",
|
"@types/video.js": "^7.3.50",
|
||||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user