diff --git a/.env b/.env deleted file mode 100644 index bfd29ef4..00000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -#API_HOST = http://localhost:7777/ -#REMOTE_CURRENT = http://localhost:7777/static/ -REACT_APP_API_HOST = https://pig.staging.vault48.org/ -REACT_APP_REMOTE_CURRENT = https://pig.staging.vault48.org/static/ -EXPOSE = 4000 diff --git a/.gitignore b/.gitignore index a0d7b1b9..1e572c99 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /npm-debug.log /.idea /dist +/.env diff --git a/package.json b/package.json index 579992cc..8d1c21b1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "connected-react-router": "^6.5.2", "date-fns": "^2.4.1", "flexbin": "^0.2.0", + "marked": "^2.0.0", "node-sass": "4.14.1", "photoswipe": "^4.1.3", "raleway-cyrillic": "^4.0.2", @@ -62,6 +63,7 @@ "devDependencies": { "@craco/craco": "5.8.0", "@types/classnames": "^2.2.7", + "@types/marked": "^1.2.2", "@types/node": "^11.13.22", "@types/ramda": "^0.26.33", "@types/react-redux": "^7.1.11", diff --git a/src/components/comment/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx index 08e9c49a..a1a48d40 100644 --- a/src/components/comment/CommentContent/index.tsx +++ b/src/components/comment/CommentContent/index.tsx @@ -49,6 +49,14 @@ const CommentContent: FC = memo( [can_edit, comment, onEditClick, onLockClick] ); + const blocks = useMemo( + () => + !!comment.text.trim() + ? formatCommentText(path(['user', 'username'], comment), comment.text) + : [], + [comment.text] + ); + return (
{comment.text && ( @@ -56,7 +64,7 @@ const CommentContent: FC = memo( {menu} - {formatCommentText(path(['user', 'username'], comment), comment.text).map( + {blocks.map( (block, key) => COMMENT_BLOCK_RENDERERS[block.type] && createElement(COMMENT_BLOCK_RENDERERS[block.type], { block, key }) diff --git a/src/components/comment/CommentTextBlock/index.tsx b/src/components/comment/CommentTextBlock/index.tsx index 930eb96a..674c7269 100644 --- a/src/components/comment/CommentTextBlock/index.tsx +++ b/src/components/comment/CommentTextBlock/index.tsx @@ -1,15 +1,20 @@ -import React, { FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { ICommentBlockProps } from '~/constants/comment'; import styles from './styles.module.scss'; +import classNames from 'classnames'; +import markdown from '~/styles/common/markdown.module.scss'; +import { formatText } from '~/utils/dom'; interface IProps extends ICommentBlockProps {} const CommentTextBlock: FC = ({ block }) => { + const content = useMemo(() => formatText(block.content), [block.content]); + return (
${block.content}

`, + __html: content, }} /> ); diff --git a/src/components/flow/Cell/index.tsx b/src/components/flow/Cell/index.tsx index b39c6246..db1f47c9 100644 --- a/src/components/flow/Cell/index.tsx +++ b/src/components/flow/Cell/index.tsx @@ -4,6 +4,7 @@ import { formatCellText, getURL } from '~/utils/dom'; import classNames from 'classnames'; import styles from './styles.module.scss'; +import markdown from '~/styles/common/markdown.module.scss'; import { Icon } from '~/components/input/Icon'; import { flowSetCellView } from '~/redux/flow/actions'; import { PRESETS } from '~/constants/urls'; @@ -118,6 +119,8 @@ const Cell: FC = ({ } }, [title]); + const cellText = useMemo(() => formatCellText(text), [text]); + return (
{is_visible && ( @@ -149,7 +152,10 @@ const Cell: FC = ({
{title &&
{title}
} - +
)} @@ -157,7 +163,10 @@ const Cell: FC = ({
{title &&
{title}
} - +
)}
diff --git a/src/components/flow/Cell/styles.module.scss b/src/components/flow/Cell/styles.module.scss index ed6d791b..8a628cc8 100644 --- a/src/components/flow/Cell/styles.module.scss +++ b/src/components/flow/Cell/styles.module.scss @@ -327,6 +327,7 @@ overflow: hidden; box-sizing: border-box; position: relative; + font-size: 6px; &::after { content: ' '; diff --git a/src/components/node/NodeTextBlock/index.tsx b/src/components/node/NodeTextBlock/index.tsx index 25f1c2b2..042b1bd7 100644 --- a/src/components/node/NodeTextBlock/index.tsx +++ b/src/components/node/NodeTextBlock/index.tsx @@ -1,19 +1,26 @@ -import React, { FC } from 'react'; -import { INode } from '~/redux/types'; +import React, { FC, useMemo } from 'react'; import { path } from 'ramda'; import { formatTextParagraphs } from '~/utils/dom'; -import styles from './styles.module.scss'; import { INodeComponentProps } from '~/redux/node/constants'; +import classNames from 'classnames'; +import styles from './styles.module.scss'; +import markdown from '~/styles/common/markdown.module.scss'; interface IProps extends INodeComponentProps {} -const NodeTextBlock: FC = ({ node }) => ( -
-); +const NodeTextBlock: FC = ({ node }) => { + const content = useMemo(() => formatTextParagraphs(path(['blocks', 0, 'text'], node)), [ + node.blocks, + ]); + + return ( +
+ ); +}; export { NodeTextBlock }; diff --git a/src/styles/common/markdown.module.scss b/src/styles/common/markdown.module.scss new file mode 100644 index 00000000..a4d1cca2 --- /dev/null +++ b/src/styles/common/markdown.module.scss @@ -0,0 +1,79 @@ +@import "../variables"; + +.wrapper { + pre { + background-color: darken($comment_bg, 2%); + border: { + left: 4px solid $red; + right: 4px solid $red; + } + font-family: monospace; + font-size: 0.9em; + border-radius: $radius; + width: 100%; + overflow: auto; + padding: $gap; + box-sizing: border-box; + margin-bottom: 0.3em; + } + + img { + max-width: 100%; + margin-bottom: 0.3em; + } + + p { + margin-bottom: 0.3em; + } + + h5, h4, h3, h2, h1 { + color: white; + font-weight: 800; + line-height: 1.2em; + margin-bottom: 0.3em; + } + + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.8em; + } + + h3 { + font-size: 1.6em; + } + + h4 { + font-size: 1.4em; + } + + h5 { + font-size: 1.2em; + } + + ul { + list-style: disc; + padding-left: 20px; + margin-bottom: 0.3em; + + li { + margin: 0.1em 0; + } + } + + strong { + font-weight: bold; + color: white; + } + + em { + font-style: italic; + } + + :global(.grey) { + color: #555555; + white-space: pre-line; + } +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 383942e2..ccbffb75 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -8,6 +8,18 @@ import Axios from 'axios'; import { PRESETS } from '~/constants/urls'; import { COMMENT_BLOCK_DETECTORS, COMMENT_BLOCK_TYPES, ICommentBlock } from '~/constants/comment'; import format from 'date-fns/format'; +import { pipe } from 'ramda'; +import { + formatTextDash, + formatExclamations, + formatTextMarkdown, + formatTextClickableUsernames, + formatTextComments, + formatTextSanitizeTags, + formatTextSanitizeYoutube, + formatTextTodos, +} from '~/utils/formatText'; +import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText'; export const getStyle = (oElm: any, strCssRule: string) => { if (document.defaultView && document.defaultView.getComputedStyle) { @@ -82,50 +94,18 @@ export const getURL = (file: Partial, size?: typeof PRESETS[keyof typeof return file?.url ? getURLFromString(file.url, size) : null; }; -export const formatText = (text: string): string => - !text - ? '' - : text - .replace( - /(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-\&\=]+)/gim, - '\n$1\n' - ) - .replace(/\n{1,}/gim, '\n') - .replace(//g, '>') - .replace( - /~([\wа-яА-Я-]+)/giu, - '~$1' - ) - .replace(/:\/\//gim, ':|--|') - .replace(/(\/\/[^\n]+)/gim, '$1') - .replace(/\/\/\s*(todo|туду):?\s*([^\n]+)/gim, '// $1 $2') - .replace( - /\/\/\s*(done|сделано|сделал|готово|fixed|пофикшено|фиксед):?\s*([^\n]+)/gim, - '// $1 $2' - ) - .replace(/(\*\*[\s\S]*?\*\*)/gim, '$1') - .replace(/(\_\_[\s\S]*?\_\_)/gim, '$1') - .replace(/(\!\![\s\S]*?(\!\!|\n|$))/gim, '$1') - .replace(/(\/\*[\s\S]*?\*\/)/gim, '$1') - .replace(/([=|-]{5,})/gim, '
') - .replace(/:\|--\|/gim, '://') - .replace( - /(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?\/=~_|!:,.;]*)[-A-Z0-9+&@#\/%=~_|])/gi, - '$1' - ) - .replace(' -- ', ' — ') - .split('\n') - .filter(el => el.trim().length) - .join('\n'); +export const formatText = pipe( + formatTextSanitizeTags, + formatTextSanitizeYoutube, + formatTextClickableUsernames, + formatTextComments, + formatTextTodos, + formatExclamations, + formatTextDash, + formatTextMarkdown +); -export const formatTextParagraphs = (text: string): string => - (text && - formatText(text) - .split('\n') - .map(str => `

${str}

`) - .join('\n')) || - null; +export const formatTextParagraphs = (text: string): string => (text && formatText(text)) || null; export const findBlockType = (line: string): ValueOf => { const match = Object.values(COMMENT_BLOCK_DETECTORS).find(detector => line.match(detector.test)); @@ -133,13 +113,16 @@ export const findBlockType = (line: string): ValueOf }; export const splitCommentByBlocks = (text: string): ICommentBlock[] => - text.split('\n').map(line => ({ + pipe( + splitTextByYoutube, + splitTextOmitEmpty + )([text]).map(line => ({ type: findBlockType(line), content: line, })); export const formatCommentText = (author: string, text: string): ICommentBlock[] => - text ? splitCommentByBlocks(formatText(text)) : null; + text ? splitCommentByBlocks(text) : null; export const formatCellText = (text: string): string => formatTextParagraphs(text); diff --git a/src/utils/formatText.ts b/src/utils/formatText.ts new file mode 100644 index 00000000..ff8e79d4 --- /dev/null +++ b/src/utils/formatText.ts @@ -0,0 +1,71 @@ +import marked from 'marked'; + +/** + * Cleans youtube urls + */ +export const formatTextSanitizeYoutube = (text: string): string => + text.replace( + /(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-\&\=]+)/gim, + '\n$1\n' + ); + +/** + * Removes HTML tags + */ +export const formatTextSanitizeTags = (text: string): string => + text.replace(//g, '>'); + +/** + * Returns clickable usernames + */ +export const formatTextClickableUsernames = (text: string): string => + text.replace( + /~([\wа-яА-Я-]+)/giu, + '~$1' + ); + +/** + * Makes gray comments + */ +export const formatTextComments = (text: string): string => + text + .replace(/:\/\//gim, ':|--|') + .replace(/(\/\/[^\n]+)/gim, '$1') + .replace(/(\/\*[\s\S]*?\*\/)/gim, '$1') + .replace(/:\|--\|/gim, '://'); + +/** + * Highlights todos + */ +export const formatTextTodos = (text: string): string => + text + .replace(/\/\/\s*(todo|туду):?\s*([^\n]+)/gim, '// $1 $2') + .replace( + /\/\/\s*(done|сделано|сделал|готово|fixed|пофикшено|фиксед):?\s*([^\n]+)/gim, + '// $1 $2' + ); + +/** + * Formats !!exclamation messages with green color + */ +export const formatExclamations = (text: string): string => + text.replace(/(\!\![\s\S]*?(\!\!|\n|$))/gim, '$1$2'); + +/** + * Formats links + */ +export const formatLinks = (text: string): string => + text.replace( + /(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?\/=~_|!:,.;]*)[-A-Z0-9+&@#\/%=~_|])/gi, + '$1' + ); + +/** + * Replaces -- with dash + */ +export const formatTextDash = (text: string): string => text.replace(' -- ', ' — '); + +/** + * Formats with markdown + */ +export const formatTextMarkdown = (text: string): string => marked(text); diff --git a/src/utils/reducer.ts b/src/utils/reducer.ts index 7af01e2d..7348d0aa 100644 --- a/src/utils/reducer.ts +++ b/src/utils/reducer.ts @@ -5,11 +5,5 @@ type Handlers> = { readonly [Type in Types]: (state: State, action: Actions) => State; }; -// export const createReducer = >( -// initialState: State, -// handlers: Handlers, -// ) => (state = initialState, action: Actions) => -// handlers.hasOwnProperty(action.type) ? handlers[action.type as Types](state, action) : state; - export const createReducer = (initialState, handlers) => (state = initialState, action) => handlers.hasOwnProperty(action.type) ? handlers[action.type](state, action) : state; diff --git a/src/utils/splitText.ts b/src/utils/splitText.ts new file mode 100644 index 00000000..32366574 --- /dev/null +++ b/src/utils/splitText.ts @@ -0,0 +1,11 @@ +import { flatten, isEmpty } from 'ramda'; + +export const splitTextByYoutube = (strings: string[]): string[] => + flatten( + strings.map(str => + str.split(/(https?:\/\/(?:www\.)(?:youtube\.com|youtu\.be)\/(?:watch)(?:\?v=)[\w\-\&\=]+)/) + ) + ); + +export const splitTextOmitEmpty = (strings: string[]): string[] => + strings.map(el => el.trim()).filter(el => !isEmpty(el)); diff --git a/yarn.lock b/yarn.lock index 5bf99ab8..81b3d1ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,6 +1684,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== +"@types/marked@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4" + integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -7129,6 +7134,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +marked@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.0.tgz#9662bbcb77ebbded0662a7be66ff929a8611cee5" + integrity sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"