diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 186aebf..4114d2e 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -10,12 +10,15 @@ "dependencies": { "@gleam-lang/highlight.js-gleam": "^1.5.0", "@sentry/browser": "^8.0.0", + "dompurify": "^3.1.4", "highlight.js": "^11.9.0", - "showdown": "^2.1.0" + "marked": "^12.0.2", + "marked-highlight": "^2.1.1" }, "devDependencies": { "@sentry/vite-plugin": "^2.16.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0", + "@types/dompurify": "^3.0.5", "dotenv": "^16.4.5", "gleam-lang": "^1.1.0", "prettier": "^3.2.5", diff --git a/apps/frontend/src/markdown.ffi.mjs b/apps/frontend/src/markdown.ffi.mjs index 47bc4eb..14429f9 100644 --- a/apps/frontend/src/markdown.ffi.mjs +++ b/apps/frontend/src/markdown.ffi.mjs @@ -1,55 +1,19 @@ +import DOMPurify from 'dompurify' import hljs from 'highlight.js/lib/core' -import showdown from 'showdown' - -export function convert(content) { - const converter = new showdown.Converter({ - extensions: [showdownHighlight({ pre: true, auto_detection: true })], - tasklists: true, +import { Marked } from 'marked' +import { markedHighlight } from 'marked-highlight' + +const parser = new Marked( + markedHighlight({ + langPrefix: 'hljs language-', + highlight(code, lang, info) { + const language = hljs.getLanguage(lang) ? lang : 'plaintext' + return hljs.highlight(code, { language }).value + }, }) - return converter.makeHtml(content) -} - -const classAttr = 'class="' - -export function showdownHighlight({ pre = false, auto_detection = true } = {}) { - const filter = text => { - const params = { - left: '
]*>',
-      right: '
', - flags: 'g', - } - - const replacement = (wholeMatch, match, left, right) => { - const lang = (left.match(/class=\"([^ \"]+)/) || [])[1] - if (!lang && !auto_detection) { - return wholeMatch - } +) - if (left.includes(classAttr)) { - const attrIndex = left.indexOf(classAttr) + classAttr.length - left = left.slice(0, attrIndex) + 'hljs ' + left.slice(attrIndex) - } else { - left = left.slice(0, -1) + ' class="hljs">' - } - - if (pre && lang) { - left = left.replace('
', `
`)
-      }
-
-      if (lang && hljs.getLanguage(lang)) {
-        return left + hljs.highlight(match, { language: lang }).value + right
-      }
-
-      return left + hljs.highlightAuto(match).value + right
-    }
-
-    return showdown.helper.replaceRecursiveRegExp(text, replacement, params.left, params.right, params.flags)
-  }
-
-  return [
-    {
-      type: 'output',
-      filter,
-    },
-  ]
+export function convert(content) {
+  const parsed = parser.parse(content)
+  return DOMPurify.sanitize(parsed, { USE_PROFILES: { html: true } })
 }
diff --git a/apps/frontend/yarn.lock b/apps/frontend/yarn.lock
index bece112..d4ff427 100644
--- a/apps/frontend/yarn.lock
+++ b/apps/frontend/yarn.lock
@@ -779,11 +779,23 @@
     javascript-natural-sort "0.7.1"
     lodash "^4.17.21"
 
+"@types/dompurify@^3.0.5":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
+  integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
+  dependencies:
+    "@types/trusted-types" "*"
+
 "@types/estree@1.0.5":
   version "1.0.5"
   resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
   integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
 
+"@types/trusted-types@*":
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+  integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
 acorn@^8.8.1:
   version "8.11.3"
   resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
@@ -930,11 +942,6 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
-commander@^9.0.0:
-  version "9.5.0"
-  resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz"
-  integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
-
 convert-source-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
@@ -956,6 +963,11 @@ debug@4, debug@^4.1.0, debug@^4.3.1:
   dependencies:
     ms "2.1.2"
 
+dompurify@^3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.4.tgz#42121304b2b3a6bae22f80131ff8a8f3f3c56be2"
+  integrity sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww==
+
 dotenv@^16.3.1, dotenv@^16.4.5:
   version "16.4.5"
   resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz"
@@ -1134,7 +1146,7 @@ has-flag@^3.0.0:
 
 highlight.js@^11.9.0:
   version "11.9.0"
-  resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
   integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
 
 https-proxy-agent@^5.0.0:
@@ -1239,6 +1251,16 @@ magic-string@0.30.8, magic-string@^0.30.2:
   dependencies:
     "@jridgewell/sourcemap-codec" "^1.4.15"
 
+marked-highlight@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/marked-highlight/-/marked-highlight-2.1.1.tgz#144120e32363f042b420276a9c7b8dab748f630f"
+  integrity sha512-ktdqwtBne8rim5mb+vvZ9FzElGFb+CHCgkx/g6DSzTjaSrVnxsJdSzB5YgCkknFrcOW+viocM1lGyIjC0oa3fg==
+
+marked@^12.0.2:
+  version "12.0.2"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e"
+  integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==
+
 minimatch@^8.0.2:
   version "8.0.4"
   resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz"
@@ -1434,13 +1456,6 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
-showdown@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz"
-  integrity sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==
-  dependencies:
-    commander "^9.0.0"
-
 signal-exit@^4.0.1:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"