From a016e981699d54ca369bbaae310dacdbdd3950cc Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Mon, 3 Oct 2022 17:30:32 +0200 Subject: [PATCH] Add katex render to Markdown Signed-off-by: Sebastian Martinez --- .gitignore | 6 ++ index.html | 1 + package-lock.json | 144 +++++++++++++++++++++++++------------- package.json | 8 ++- scripts/copy-katex-assets | 5 ++ src/Markdown.svelte | 16 +---- src/utils.ts | 40 +++++++---- 7 files changed, 142 insertions(+), 78 deletions(-) create mode 100755 scripts/copy-katex-assets diff --git a/.gitignore b/.gitignore index 839130f5b..4f6aa0fc6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ build/ node_modules/ NOTES +# KaTeX files +*.min.css +KaTeX_**.ttf +KaTeX_**.woff +KaTeX_**.woff2 + # Integration Tests cypress/screenshots diff --git a/index.html b/index.html index 0e52164c1..66baff00b 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,7 @@ + diff --git a/package-lock.json b/package-lock.json index e9bf16c3f..111d274fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "packages": { "": { "version": "1.0.0", + "hasInstallScript": true, "dependencies": { "@datamodels/identity-profile-basic": "^0.1.2", "@ethersproject/abstract-provider": "^5.4.0", @@ -19,8 +20,10 @@ "@types/md5": "^2.3.2", "@walletconnect/client": "^1.8.0", "buffer": "^6.0.3", + "dompurify": "^2.4.0", "ethers": "^5.7.1", "events": "^3.3.0", + "katex": "^0.16.2", "lodash": "^4.17.21", "lru-cache": "^7.14.0", "marked": "^4.1.0", @@ -31,13 +34,14 @@ "siwe": "^2.0.5", "svelte": "^3.50.1", "svelte-preprocess": "^4.10.7", - "svelte-routing": "^1.6.0", - "xss": "^1.0.14" + "svelte-routing": "^1.6.0" }, "devDependencies": { "@rsksmart/mock-web3-provider": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.8", "@tsconfig/svelte": "^3.0.0", + "@types/dompurify": "^2.3.4", + "@types/katex": "^0.14.0", "@types/lodash": "^4.14.186", "@typescript-eslint/eslint-plugin": "^5.38.1", "cypress": "^10.9.0", @@ -1444,6 +1448,15 @@ "@types/node": "*" } }, + "node_modules/@types/dompurify": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.4.tgz", + "integrity": "sha512-EXzDatIb5EspL2eb/xPGmaC8pePcTHrkDCONjeisusLFrVfl38Pjea/R0YJGu3k9ZQadSvMqW0WXPI2hEo2Ajg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/form-data": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", @@ -1459,6 +1472,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.186", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", @@ -1527,6 +1546,12 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, "node_modules/@types/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", @@ -2799,11 +2824,6 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, - "node_modules/cssfilter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", - "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" - }, "node_modules/cypress": { "version": "10.9.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz", @@ -3070,6 +3090,11 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4947,6 +4972,29 @@ "verror": "1.10.0" } }, + "node_modules/katex": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", + "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, "node_modules/keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -7239,26 +7287,6 @@ } } }, - "node_modules/xss": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", - "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", - "dependencies": { - "commander": "^2.20.3", - "cssfilter": "0.0.10" - }, - "bin": { - "xss": "bin/xss" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/xss/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -8281,6 +8309,15 @@ "@types/node": "*" } }, + "@types/dompurify": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.4.tgz", + "integrity": "sha512-EXzDatIb5EspL2eb/xPGmaC8pePcTHrkDCONjeisusLFrVfl38Pjea/R0YJGu3k9ZQadSvMqW0WXPI2hEo2Ajg==", + "dev": true, + "requires": { + "@types/trusted-types": "*" + } + }, "@types/form-data": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", @@ -8296,6 +8333,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", + "dev": true + }, "@types/lodash": { "version": "4.14.186", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.186.tgz", @@ -8364,6 +8407,12 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, "@types/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", @@ -9333,11 +9382,6 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, - "cssfilter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", - "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" - }, "cypress": { "version": "10.9.0", "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz", @@ -9555,6 +9599,11 @@ "esutils": "^2.0.2" } }, + "dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -10918,6 +10967,21 @@ "verror": "1.10.0" } }, + "katex": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.2.tgz", + "integrity": "sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==", + "requires": { + "commander": "^8.0.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + } + } + }, "keccak": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", @@ -12553,22 +12617,6 @@ "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", "requires": {} }, - "xss": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", - "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", - "requires": { - "commander": "^2.20.3", - "cssfilter": "0.0.10" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - } - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 3d4153b14..0c14fe364 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "start": "vite", "serve": "vite preview", "build": "scripts/build", + "postinstall": "scripts/copy-katex-assets", "check": "scripts/check", "format": "npx prettier '**/*.@(ts|js|svelte|json|css|html)' --ignore-path .gitignore --write", "test:unit": "TZ='UTC' vitest run", @@ -19,6 +20,8 @@ "@rsksmart/mock-web3-provider": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.8", "@tsconfig/svelte": "^3.0.0", + "@types/dompurify": "^2.3.4", + "@types/katex": "^0.14.0", "@types/lodash": "^4.14.186", "@typescript-eslint/eslint-plugin": "^5.38.1", "cypress": "^10.9.0", @@ -46,8 +49,10 @@ "@types/md5": "^2.3.2", "@walletconnect/client": "^1.8.0", "buffer": "^6.0.3", + "dompurify": "^2.4.0", "ethers": "^5.7.1", "events": "^3.3.0", + "katex": "^0.16.2", "lodash": "^4.17.21", "lru-cache": "^7.14.0", "marked": "^4.1.0", @@ -58,7 +63,6 @@ "siwe": "^2.0.5", "svelte": "^3.50.1", "svelte-preprocess": "^4.10.7", - "svelte-routing": "^1.6.0", - "xss": "^1.0.14" + "svelte-routing": "^1.6.0" } } diff --git a/scripts/copy-katex-assets b/scripts/copy-katex-assets new file mode 100755 index 000000000..7c7b114cd --- /dev/null +++ b/scripts/copy-katex-assets @@ -0,0 +1,5 @@ +#!/bin/bash +set -Eeuo pipefail + +cp -r node_modules/katex/dist/katex.min.css public/katex.min.css +cp -r node_modules/katex/dist/fonts/* public/fonts/ diff --git a/src/Markdown.svelte b/src/Markdown.svelte index f5d9755c0..d6f02bd1e 100644 --- a/src/Markdown.svelte +++ b/src/Markdown.svelte @@ -8,7 +8,7 @@ getImageMime, isUrl, } from "@app/utils"; - import xss, { getDefaultWhiteList } from "xss"; + import dompurify from "dompurify"; export let content: string; export let getImage: (path: string) => Promise; @@ -19,18 +19,8 @@ let container: HTMLElement; - const render = (content: string): string => { - return xss(marked.parse(content), { - whiteList: { - ...getDefaultWhiteList(), - img: ["src", "alt", "title"], - audio: ["src"], - video: ["src"], - a: ["href", "name"], - }, - stripIgnoreTag: false, - }); - }; + const render = (content: string): string => + dompurify.sanitize(marked.parse(content)); onMount(() => { // Don't underline tags that contain images. diff --git a/src/utils.ts b/src/utils.ts index e993c3cbd..8280129d7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import type { RouteLocation } from "@app/index"; import md5 from "md5"; import { BigNumber } from "ethers"; import multibase from "multibase"; +import katex from "katex"; import multihashes from "multihashes"; import type { TransactionResult } from "@gnosis.pm/safe-core-sdk"; import EthersSafe, { EthersAdapter } from "@gnosis.pm/safe-core-sdk"; @@ -808,33 +809,42 @@ export const unreachable = (value: never): never => { throw new Error(`Unreachable code: ${value}`); }; -const markdownEmojiTokenizer: marked.TokenizerExtension = { +const emojisMarkedExtension = { name: "emoji", level: "inline", - start(src: string) { - return src.indexOf(":"); - }, + start: (src: string) => src.indexOf(":"), tokenizer(src: string) { - const rule = /^:([\w+-]+):/; - const match = rule.exec(src); + const match = src.match(/^:([\w+-]+):/); if (match) { return { type: "emoji", raw: match[0], - emoji: match[1], + text: match[1].trim(), }; } }, + renderer: (token: marked.Tokens.Generic) => + `${parseEmoji(token.text)}`, }; -const markdownEmojiRenderer: marked.RendererExtension = { - name: "emoji", - renderer(token: marked.Tokens.Generic) { - return `${parseEmoji(token.emoji)}`; +const katexMarkedExtension = { + name: "katex", + level: "inline", + start: (src: string) => src.indexOf("$"), + tokenizer(src: string) { + const match = src.match(/^\$+([^$\n]+?)\$+/); + if (match) { + return { + type: "katex", + raw: match[0], + text: match[1].trim(), + }; + } }, + renderer: (token: marked.Tokens.Generic) => + katex.renderToString(token.text, { + throwOnError: false, + }), }; -export const markdownExtensions = [ - markdownEmojiTokenizer, - markdownEmojiRenderer, -]; +export const markdownExtensions = [emojisMarkedExtension, katexMarkedExtension];