diff --git a/package-lock.json b/package-lock.json index ae8c27a2b00569114e537719e3a120124ff84acb..039a56eb74f70bd05ba34720ae27ad8fe2405585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4458,6 +4458,11 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" + }, "cssnano": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", @@ -14977,6 +14982,15 @@ "@babel/runtime-corejs3": "^7.8.3" } }, + "xss": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", + "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index fe1ec7035b92d15b03b5b46660db6d8b40fcbd27..ce86b5863423cefb4b5ce769bed4bff74a06846e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "react-scripts": "3.4.3", "showdown": "^1.9.1", "unfetch": "^4.2.0", - "wait-queue": "^1.1.4" + "wait-queue": "^1.1.4", + "xss": "^1.0.8" }, "scripts": { "start": "react-scripts start", @@ -58,7 +59,7 @@ "^@?\\w" ], [ - "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|ws)(/.*|$)" + "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|markdown|ws)(/.*|$)" ], [ "^(test-utils)(/.*|$)" diff --git a/src/components/annoucements/Announcement.jsx b/src/components/annoucements/Announcement.jsx index 95973cc4ff7fb264ec4c30e33181986fac5b8328..0852cad74098905a0bd68f0217500038ddc181ec 100644 --- a/src/components/annoucements/Announcement.jsx +++ b/src/components/annoucements/Announcement.jsx @@ -95,7 +95,11 @@ const Announcement = ({ )} </div> {canRunActions && ( - <DropdownMenu right triggerIconClass="ico--dots-three-horizontal"> + <DropdownMenu + right + className="pl-4" + triggerIconClass="ico--dots-three-horizontal" + > {showEdit && ( <DropdownMenuItem onClick={onEdit} diff --git a/src/components/mde/MarkdownEditor.jsx b/src/components/mde/MarkdownEditor.jsx index 88a74fe4d50d38f0dbcea58e67d6e9bd5bc99de9..f2ff08973950087196d358d40fcd1f9a46d2dfda 100644 --- a/src/components/mde/MarkdownEditor.jsx +++ b/src/components/mde/MarkdownEditor.jsx @@ -1,18 +1,12 @@ import React, { useState } from "react"; import ReactMde from "react-mde"; import classNames from "classnames"; -import Showdown from "showdown"; + +import { markdownConverter } from "markdown"; import "react-mde/lib/styles/css/react-mde-toolbar.css"; import "./MarkdownEditor.css"; -const converter = new Showdown.Converter({ - tables: true, - simplifiedAutoLink: true, - strikethrough: true, - tasklists: true, -}); - const MarkdownEditor = ({ value, onChange, @@ -47,7 +41,7 @@ const MarkdownEditor = ({ selectedTab={selectedTab} onTabChange={setSelectedTab} generateMarkdownPreview={(markdown) => - Promise.resolve(converter.makeHtml(markdown)) + Promise.resolve(markdownConverter.makeHtml(markdown)) } classes={classes} l18n={l18n} diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 72ed9925703251ea0ab18032730d8a93f37db9a6..3a57d1e219f75140f2b2e9d0084dd7d13f6663ca 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -135,6 +135,10 @@ const Post = ({ const showHideAction = !archived; const showArchiveAction = !archived; + const htmlContent = { + __html: content, + }; + return ( <div className={wrapperClassName} ref={ref}> <img @@ -164,7 +168,7 @@ const Post = ({ {labels} </div> </div> - <div className="flex items-center space-x-4"> + <div className="flex items-center"> <Thumbs likes={ranking.likes} dislikes={ranking.dislikes} @@ -174,7 +178,7 @@ const Post = ({ myVote={ranking.myVote} /> {canRunActions && ( - <DropdownMenu right> + <DropdownMenu right className="pl-4"> {showAnnounceAction && ( <DropdownMenuItem onClick={onAnnounceProcedureProposal} @@ -239,9 +243,10 @@ const Post = ({ <div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2"> {labels} </div> - <p className="text-sm lg:text-base text-black leading-normal"> - {content} - </p> + <div + className="text-sm lg:text-base text-black leading-normal content-block" + dangerouslySetInnerHTML={htmlContent} + ></div> </div> </div> ); diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx index c8143ece2ba6ffa9a911990b0ab0289ee7f93482..da1ad33890b159d90d45442aff4f3f49b12df94a 100644 --- a/src/components/posts/PostList.jsx +++ b/src/components/posts/PostList.jsx @@ -56,7 +56,7 @@ const PostList = ({ author={item.author} type={item.type} state={item.state} - content={item.content} + content={item.contentHtml} ranking={item.ranking} historyLog={item.historyLog} modified={item.modified} diff --git a/src/markdown.js b/src/markdown.js new file mode 100644 index 0000000000000000000000000000000000000000..559427cef5a0542cd2e59a33f18b1465acf1adf4 --- /dev/null +++ b/src/markdown.js @@ -0,0 +1,21 @@ +import Showdown from "showdown"; +import xss from "xss"; + +const xssFilter = (converter) => [ + { + type: "output", + filter: (text) => xss(text), + }, +]; + +export const markdownConverter = new Showdown.Converter({ + tables: true, + simplifiedAutoLink: true, + strikethrough: true, + tasklists: true, + omitExtraWLInCodeBlocks: true, + noHeaderId: true, + headerLevelStart: 2, + openLinksInNewWindow: true, + extensions: [xssFilter], +}); diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index c3740ac3cdc37bd46c21f50741539426063a7203..6e15e30fed9379ffb05dd879e6e94d6db1c3c92f 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -151,7 +151,7 @@ const Home = () => { <h1 className="head-alt-md lg:head-alt-lg mb-0"> Bod č. {programEntry.number}: {programEntry.title} </h1> - <DropdownMenu right triggerSize="lg"> + <DropdownMenu right triggerSize="lg" className="pl-4"> <DropdownMenuItem onClick={() => setShowProgramEditModal(true)} icon="ico--edit-pencil" diff --git a/src/utils.js b/src/utils.js index 83fb40083240e42090dc552b3dd27bc12d4063f3..a35571edb8d2ee1e42907d0f81dff022295c8ea4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,6 +3,8 @@ import pick from "lodash/pick"; import property from "lodash/property"; import values from "lodash/values"; +import { markdownConverter } from "markdown"; + /** * Filter & sort collection of posts. * @param {CF2021.PostStoreFilters} filters @@ -120,6 +122,7 @@ export const announcementTypeMappingRev = { export const parseRawPost = (rawPost) => { const post = { ...pick(rawPost, ["id", "content", "author"]), + contentHtml: markdownConverter.makeHtml(rawPost.content), datetime: new Date(rawPost.datetime), historyLog: rawPost.history_log, ranking: { diff --git a/src/ws/handlers/posts.js b/src/ws/handlers/posts.js index 3c07ed0677243efddaa841762afffaf82d9c8fcd..1eb6dc7d898aecef4d676a5560a056b093ff0793 100644 --- a/src/ws/handlers/posts.js +++ b/src/ws/handlers/posts.js @@ -1,5 +1,6 @@ import has from "lodash/has"; +import { markdownConverter } from "markdown"; import { PostStore } from "stores"; import { parseRawPost, postsStateMapping, updateWindowPosts } from "utils"; @@ -24,6 +25,9 @@ export const handlePostChanged = (payload) => { if (state.items[payload.id]) { if (has(payload, "content")) { state.items[payload.id].content = payload.content; + state.items[payload.id].contentHtml = markdownConverter.makeHtml( + payload.content + ); state.items[payload.id].modified = true; } diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts index 7c0d20197581b1952f9069d73ce8999558d62e59..fbed0e076d90270ed75dfc7abd2134f954a2252f 100644 --- a/typings/cf2021.d.ts +++ b/typings/cf2021.d.ts @@ -80,6 +80,7 @@ declare namespace CF2021 { }; type: PostType; content: string; + contentHtml: string; ranking: { score: number; likes: number;