From eb44a4e26e98a24dd48dec0fcb2806e391235e1b Mon Sep 17 00:00:00 2001 From: Pauline Didier Date: Fri, 9 Aug 2024 15:24:49 +0200 Subject: [PATCH] feat: extend markdown support --- .../model/panels/DescriptionPreview.js | 96 +++++++++++++++++++ .../model/panels/left/ComponentTab.js | 16 ++-- .../model/panels/left/DescriptionPreview.js | 63 ------------ .../model/panels/right/EditableTypography.js | 59 +++++++++--- .../model/panels/right/Suggestion.js | 69 +++++++++++-- 5 files changed, 216 insertions(+), 87 deletions(-) create mode 100644 app/src/components/model/panels/DescriptionPreview.js delete mode 100644 app/src/components/model/panels/left/DescriptionPreview.js diff --git a/app/src/components/model/panels/DescriptionPreview.js b/app/src/components/model/panels/DescriptionPreview.js new file mode 100644 index 00000000..7e28db25 --- /dev/null +++ b/app/src/components/model/panels/DescriptionPreview.js @@ -0,0 +1,96 @@ +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import Box from "@mui/material/Box"; + +marked.use({ + extensions: [ + { + name: "heading", + renderer({ text, depth }) { + return `${text}`; + }, + }, + { + name: "image", + renderer(_) { + return "Images are not supported"; + }, + }, + { + name: "link", + renderer(token) { + if (!token.href) { + return `

${token.text} (link without href)

`; + } + return `${token.text}`; + }, + }, + { + name: "code", + renderer(token) { + if (!token.text) { + return ""; + } + if (token.lang) { + return `

\`\`\`${token.lang}

${token.text}

\`\`\`

`; + } + return `

\`\`\`

${token.text}

\`\`\`

`; + }, + }, + { + name: "list", + renderer(token) { + const itemList = token.items.map((i) => { + return ( + "
  • " + i.raw.replace(/\n+$/, "") + "
  • " + ); + }); + if (token.ordered) { + return `
      ${itemList.join("\n")}
    `; + } else { + return ``; + } + }, + }, + ], +}); + +const domPurityConfig = { + USE_PROFILES: { html: true }, + FORBID_TAGS: ["img"], + ADD_ATTR: ["target"], +}; + +export const DescriptionPreview = ({ + description, + readOnly, + showDescriptionTextField, + sx = {}, +}) => { + if (!description) { + showDescriptionTextField(true); + return null; + } + + const sanitizedHtml = DOMPurify.sanitize( + marked.parse(description), + domPurityConfig + ); + + if (readOnly) { + return ( + + ); + } else { + return ( + + ); + } +}; diff --git a/app/src/components/model/panels/left/ComponentTab.js b/app/src/components/model/panels/left/ComponentTab.js index ff046266..c9f655c6 100644 --- a/app/src/components/model/panels/left/ComponentTab.js +++ b/app/src/components/model/panels/left/ComponentTab.js @@ -14,8 +14,7 @@ import { MultipleSystemsDropdown } from "../../../elements/MultipleSystemsDropdo import { COMPONENT_TYPE } from "../../board/constants"; import { useSelectedComponent } from "../../hooks/useSelectedComponent"; import { TechStacksDropdown } from "./TechStackDropdown"; -import { DescriptionPreview } from "./DescriptionPreview"; -import { set } from "lodash"; +import { DescriptionPreview } from "../DescriptionPreview"; export function ComponentTab() { const dispatch = useDispatch(); @@ -214,11 +213,14 @@ export function ComponentTab() { /> )} {(showDescriptionPreview || readOnly) && ( - + <> + Description + + )} diff --git a/app/src/components/model/panels/left/DescriptionPreview.js b/app/src/components/model/panels/left/DescriptionPreview.js deleted file mode 100644 index 0a802948..00000000 --- a/app/src/components/model/panels/left/DescriptionPreview.js +++ /dev/null @@ -1,63 +0,0 @@ -import { marked } from "marked"; -import DOMPurify from "dompurify"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; - -marked.use({ - extensions: [ - { - name: "heading", - renderer({ text, depth }) { - return `${text}`; - }, - }, - { - name: "image", - renderer(_) { - return "Images are not supported"; - }, - }, - { - name: "list", - renderer(token) { - const itemList = token.items.map((i) => { - return ( - "
  • " + i.raw.replace(/\n+$/, "") + "
  • " - ); - }); - if (token.ordered) { - return `
      ${itemList.join("\n")}
    `; - } else { - return `
      ${itemList.join("\n")}
    `; - } - }, - }, - ], -}); - -export const DescriptionPreview = ({ - description, - showDescriptionTextField, - readOnly, -}) => { - const sanitizedHtml = DOMPurify.sanitize(marked.parse(description)); - - if (readOnly) { - return ( - <> - Description - - - ); - } else { - return ( - <> - Description - - - ); - } -}; diff --git a/app/src/components/model/panels/right/EditableTypography.js b/app/src/components/model/panels/right/EditableTypography.js index da492ce4..5ead7fd3 100644 --- a/app/src/components/model/panels/right/EditableTypography.js +++ b/app/src/components/model/panels/right/EditableTypography.js @@ -1,5 +1,48 @@ import { Input, Typography } from "@mui/material"; import { useEffect, useState } from "react"; +import Box from "@mui/material/Box"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import { DescriptionPreview } from "../DescriptionPreview"; + +marked.use({ + extensions: [ + { + name: "heading", + renderer({ text, depth }) { + return `${text}`; + }, + }, + { + name: "image", + renderer(_) { + return "Images are not supported"; + }, + }, + { + name: "link", + renderer(token) { + console.log({ token }); + return `${token.text}`; + }, + }, + { + name: "list", + renderer(token) { + const itemList = token.items.map((i) => { + return ( + "
  • " + i.raw.replace(/\n+$/, "") + "
  • " + ); + }); + if (token.ordered) { + return `
      ${itemList.join("\n")}
    `; + } else { + return `
      ${itemList.join("\n")}
    `; + } + }, + }, + ], +}); export function EditableTypography({ text, @@ -63,14 +106,8 @@ export function EditableTypography({ }} /> ) : ( - { - if (!readOnly) { - setIsEditing(!isEditing); - } - }} + - {value || placeholder} - + readOnly={readOnly} + showDescriptionTextField={() => setIsEditing(!isEditing)} + /> )} ); diff --git a/app/src/components/model/panels/right/Suggestion.js b/app/src/components/model/panels/right/Suggestion.js index 4c953ff7..33919de2 100644 --- a/app/src/components/model/panels/right/Suggestion.js +++ b/app/src/components/model/panels/right/Suggestion.js @@ -15,6 +15,65 @@ import { } from "../../../../api/gram/suggestions"; import { useModelID } from "../../hooks/useModelID"; import { useSelectedComponent } from "../../hooks/useSelectedComponent"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import { DescriptionPreview } from "../DescriptionPreview"; + +function SuggestionDescription({ description }) { + marked.use({ + extensions: [ + { + name: "heading", + renderer({ text, depth }) { + return `${text}`; + }, + }, + { + name: "image", + renderer(_) { + return "Images are not supported"; + }, + }, + { + name: "list", + renderer(token) { + const itemList = token.items.map((i) => { + return ( + "
  • " + + i.raw.replace(/\n+$/, "") + + "
  • " + ); + }); + if (token.ordered) { + return `
      ${itemList.join("\n")}
    `; + } else { + return `
      ${itemList.join("\n")}
    `; + } + }, + }, + ], + }); + + const domPurityConfig = { + USE_PROFILES: { html: true }, + FORBID_TAGS: ["img", "table", "tr", "td", "th"], + }; + + const sanitizedHtml = DOMPurify.sanitize( + marked.parse(description), + domPurityConfig + ); + + return ( + + + Description: + +
    + {description} +
    + ); +} function SuggestionMitigations({ suggestion, threatSuggestions }) { const threatsMitigated = suggestion?.mitigates?.filter((m) => @@ -81,17 +140,15 @@ export function Suggestion({ suggestion, rejected, readOnly, isControl }) { {suggestion.title} {suggestion.description && ( - - {suggestion.description} - + /> )}