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"